# Exercise Session 1: Getting Started with Computer Vision

The goals of this exercise are:
* getting started with Python for image manipulation
* getting familiar with the basic image manipulation functions
* implementing some simple real-world Computer Vision algorithms

```python
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
import cv2

from skimage import measure, color
from os import listdir

plt.rcParams['figure.figsize'] = (10, 10)
plt.rcParams['image.cmap'] = 'gray'
```

If you have some missing packages, you can use the corresponding ```conda install``` command from below list to install it,

```python
conda install -c conda-forge scikit-image
conda install -c conda-forge matplotlib
conda install -c conda-forge opencv
conda install -c anaconda numpy
```

## Exercise 1: Image Segmentation

In many vision applications, it is useful to separate out the regions of the image corresponding to objects in which we are interested in the regions of the image that correspond to the background. Thresholding often provides an easy and convenient way to perform this segmentation on the basis of the different intensities or colours in the foreground and background regions of an image.

The input to a thresholding operation is typically a grayscale or colour image. In the simplest implementation, the output is a binary image representing the segmentation. Black pixels correspond to background and white pixels correspond to foreground (or vice versa). Multiple thresholds can be specified, so that a band of intensity values can be set to white while everything else is set to black.

If it is possible to separate out the foreground of an image on the basis of pixel intensity, then the intensity of pixels within foreground objects must be distinctly different from the intensity of pixels within the background. In this case, we expect to see a distinct peak in the histogram corresponding to foreground objects such that thresholds can be chosen to isolate this peak accordingly. If such a peak does not exist, then it is unlikely that simple thresholding will produce a good segmentation.

* Read and display the image "wdg.png" using function ```cv2.imread()```. Convert it from color to greyscale if necessary using function ```cv2.cvtColor()```. Visualize the results using ```plt.imshow()``` function.

```python
#add your code here
```

* Write a function to threshold a gray scale image by using two threshold values as shown above. The values must satisfy the following conditions:
\begin{align}
Th1 < Th2
\newline
Th1 > 0
\newline
Th2 < I_{max}
\end{align}
where $I_{max}$ is the maximum intensity of the image.

```python
#add your code here
```

* Take a look at the pixels intensity histogram using function ```matplotlib.pyplot.hist()``` and choose the best threshold values and segment the image.

```python
#add your code here
```

* Repeat the same steps for images "brain.png" and "shading.png". What do you notice? What are the drawbacks of this segmentation method?

```python
#add your code here
```

## Exercise 2: Background Substraction

Background subtraction is an important preprocessing step of many algorithms, e.g. object detection. In the following exercises we will try to subtract the scene background using multiple images.

### 2.1 Extracting a moving object

* Load the "street1.tiff" and the "street2.tiff" images. Visualize them.

```python
#add your code here.
```

* Transform the 8-bit images into float images. You can use image attribute ```dtype``` to check the type of image. To perform the type casting you can use ```np.float32()``` function.

```python
#add your code here.
```

* Subtract the second image from the first one using basic matrix arithmetic operations. Visualize the results. Why was it important to do the casting before subtracting the images?

```python
# add your code here. Assign the difference to 'image_diff'
plt.imshow(image_diff)
```

### 2.2 Building a background model
For this exercise, you are given a sequence of images that contains pedestrians and we wish to segment with a background subtraction algorithm.

* Load and create a stack of images from the images inside ```images/sequence1```. Build a "background model" by averaging out the set of given images. Detect pedestrians
 subtracting the background model from the original images and applying the right threshold.

```python
sq_of_images = listdir('images/sequence1/')
sq_of_images = [img for img in sq_of_images if img.endswith(".jpg")]

# 'sq_of_images' holds the list of image names. Create an image stack using them.

# Compute the mean image using the stack and assign it to 'mean_image'.
plt.imshow(mean_image)
plt.title('Background model')
```

```python
T = 0.1
plt.figure(2)
plt.suptitle('Pedestrians')

# First convert the mean image to grayscale.
# When subtracting, each image must also be converted to grayscale.

for i in range(len(stack)):
    # Compute the foreground image here. Assign it to 'foreground' variable.
    plt.subplot(6,5,i+1)
    plt.imshow(foreground)
    plt.axis('off')
    plt.title('Image: ' + str(i+1))
```

* Create a more sophisticated background model, where each pixel can be modeled with a Gaussian distribution. We can classify a pixel as background if its current intensity ($I_t$) lies within some confidence interval of its distribution’s mean ($\mu(t)$):

\begin{align}
\frac{\mid{(I_t - \mu_t)}\mid}{\sigma_t} > T \rightarrow Foreground
\newline
\frac{\mid{(I_t - \mu_t)}\mid}{\sigma_t} < T \rightarrow Background
\end{align}

$\sigma_t$ is the standard deviation of the pixel $t$ in the background model. $T$ is the threshold.

```python
# Add your code here. Before computing the model convert the images into gray-scale images.
```

What difference do you notice between the two approaches? How does changing
the threshold affect them?

## Exercise 3: Connected Components

Segmentation can be also done for colour images. It is also often a first step for the further analysis e.g. measuring properties of the object. Here our goal is to count the number of apples in the image below.

* Read and display"apples.jpg" image.

```python
#add your code here
```

* Check the size of the image. Compared to the previous images it should have an additional dimension corresponding to three colour channels: red, green and blue. Visualize those 3 channels separately.

```python
fig, axes = plt.subplots(1, 3)
fig.set_size_inches(18.5, 10.5)
# add your code for visualizing three channels
```

* Try to obtain a binary image such that binary image == 1 for pixels representing apples and 0 otherwise. Which channel(s) would you use for that?

```python
bin_img = np.zeros(img_apples.shape[0:2])
#add your code fot thresholding the image
plt.imshow(bin_img)
```

* Count the number of connected components in your binary image (here corresponding to apples). For this, you can use function ```measure.label()```. Its output is an array of the same size as input binary image, with each pixel assigned to a different connected component (ID). Visualize the image with detected connected components.

```python
#add your code to find connected components
# labels =
```

```python
plt.imshow(labels,cmap="jet")
```

* Simple thresholding sometimes leads to detecting also noise in the background that is detected as seperate connected components. Try to suppress the noise by removing all connected components smaller than a user-defined threshold.

```python
def remove_noise(label_img,threshold):

#add your code here

    return label_img_new
```

```python
labels_new = remove_noise(labels,50)
plt.imshow(labels_new,cmap="jet")
```


# Graph Attention Network

\[[paper](https://arxiv.org/abs/1710.10903)\] , \[[original code](https://github.com/PetarV-/GAT)\] , \[[all other implementations](https://paperswithcode.com/paper/graph-attention-networks)\]

From [Graph Convolutional Network (GCN)](https://arxiv.org/abs/1609.02907), we learned that combining local graph structure and node-level features yields good performance on node classification task. Hwever, the way GCN aggregates is structure-dependent, which may hurt its generalizability.

One workaround is to simply average over all neighbor node features as in GraphSAGE. Graph Attention Network proposes an alternative way by weighting neighbor features with feature dependent and structure free normalization, in the style of attention.

The goal of this tutorial:

- Explain what is Graph Attention Network.
- Understand the attentions learnt.
- Introduce to inductive learning.

Introducing Attention to GCN
----------------------------

The key difference between GAT and GCN is how the information from the one-hop neighborhood is aggregated.

For GCN, a graph convolution operation produces the normalized sum of the node features of neighbors:

$$h_i^{(l+1)}=\sigma\left(\sum_{j\in \mathcal{N}(i)} {\frac{1}{c_{ij}} W^{(l)}h^{(l)}_j}\right)$$

where $\mathcal{N}(i)$ is the set of its one-hop neighbors (to include $v_i$ in the set, simply add a self-loop to each node),
$c_{ij}=\sqrt{|\mathcal{N}(i)|}\sqrt{|\mathcal{N}(j)|}$ is a normalization constant based on graph structure, $\sigma$ is an activation function (GCN uses ReLU), and $W^{(l)}$ is a shared weight matrix for node-wise feature transformation. Another model proposed in
[GraphSAGE](https://www-cs-faculty.stanford.edu/people/jure/pubs/graphsage-nips17.pdf)
employs the same update rule except that they set
$c_{ij}=|\mathcal{N}(i)|$.

GAT introduces the attention mechanism as a substitute for the statically
normalized convolution operation. Below are the equations to compute the node
embedding $h_i^{(l+1)}$ of layer $l+1$ from the embeddings of
layer $l$:

```python
!pip install -q --upgrade git+https://github.com/mlss-skoltech/tutorials_week2.git#subdirectory=graph_neural_networks
```

```python
import pkg_resources

ZIP_PATH = pkg_resources.resource_filename('gnnutils', 'data/data.zip')
DATA_PATH = './data'

!unzip -u {ZIP_PATH} -d ./
```

    Archive:  /anaconda3/lib/python3.7/site-packages/gnnutils/data/data.zip
      inflating: ./data/ind.cora.ally
      inflating: ./data/ind.cora.test.index
      inflating: ./data/ind.pubmed.graph
      inflating: ./data/ind.cora.allx
      inflating: ./data/ind.citeseer.test.index
      inflating: ./data/ind.citeseer.ty
      inflating: ./data/ind.citeseer.tx
      inflating: ./data/ind.citeseer.graph
      inflating: ./data/preprocessed_MNIST.dump
      inflating: ./data/ind.cora.tx
      inflating: ./data/ind.pubmed.test.index
      inflating: ./data/ind.cora.ty
      inflating: ./data/ind.cora.graph
      inflating: ./data/ind.citeseer.y
      inflating: ./data/ind.citeseer.ally
      inflating: ./data/ind.citeseer.allx
      inflating: ./data/ind.citeseer.x
      inflating: ./data/ind.pubmed.ally
      inflating: ./data/ind.pubmed.allx
      inflating: ./data/ind.pubmed.x
      inflating: ./data/ind.pubmed.tx
      inflating: ./data/ind.cora.x
      inflating: ./data/ind.pubmed.ty
      inflating: ./data/ind.pubmed.y
      inflating: ./data/ind.cora.y

```python
import os,sys,inspect
import os
import joblib
import tensorflow as tf
import numpy as np
import h5py
import scipy.sparse.linalg as la
import scipy.sparse as sp
import scipy
import time
import pickle

import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
%matplotlib inline

import scipy.io as sio

from gnnutils import process_data
```

```python
def count_no_weights():
    total_parameters = 0
    for variable in tf.trainable_variables():
        # shape is an array of tf.Dimension
        shape = variable.get_shape()
        variable_parameters = 1
        for dim in shape:
            variable_parameters *= dim.value
        total_parameters += variable_parameters
    print('#weights in the model: %d' % (total_parameters,))

def frobenius_norm(tensor):
    square_tensor = tf.square(tensor)
    tensor_sum = tf.reduce_sum(square_tensor)
    frobenius_norm = tf.sqrt(tensor_sum)
    return frobenius_norm

```

```python
class GAT:

    """
    The neural network model.
    """
    def __init__(self, idx_rows, idx_cols, A_shape, X, Y, num_hidden_feat, n_heads, learning_rate=5e-2, gamma=1e-3, idx_gpu = '/gpu:3'):

        self.num_hidden_feat = num_hidden_feat
        self.learning_rate = learning_rate
        self.gamma=gamma
        with tf.Graph().as_default() as g:
                self.graph = g

                with tf.device(idx_gpu):

                        # list of weights' tensors l2-loss
                        self.regularizers = []

                        #definition of constant matrices
                        self.X = tf.constant(X, dtype=tf.float32)
                        self.Y = tf.constant(Y, dtype=tf.float32)

                        #placeholder definition
                        self.idx_nodes = tf.placeholder(tf.int32)
                        self.keep_prob = tf.placeholder(tf.float32)

                        #model definition

                        self.X0 = []
                        for k in range(n_heads):
                            with tf.variable_scope('GCL_1_{}'.format(k+1)):
                                self.X0.append(self.GAT_layer(self.X, num_hidden_feat, idx_rows, idx_cols, A_shape, tf.nn.elu))
                        self.X0 = tf.concat(self.X0, 1)

                        with tf.variable_scope('GCL_2'):
                            self.logits = self.GAT_layer(self.X0, Y.shape[1], idx_rows, idx_cols, A_shape, tf.identity)

                        self.l_out = tf.gather(self.logits, self.idx_nodes)
                        self.c_Y = tf.gather(self.Y, self.idx_nodes)

                        #loss function definition
                        self.l2_reg = tf.reduce_sum(self.regularizers)
                        self.data_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=self.l_out, labels=self.c_Y))

                        self.loss = self.data_loss + self.gamma*self.l2_reg

                        #solver definition
                        self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
                        self.opt_step = self.optimizer.minimize(self.loss)

                        #predictions and accuracy extraction
                        self.c_predictions = tf.argmax(tf.nn.softmax(self.l_out), 1)
                        self.accuracy = tf.contrib.metrics.accuracy(self.c_predictions, tf.argmax(self.c_Y, 1))

                        #gradients computation
                        self.trainable_variables = tf.trainable_variables()
                        self.var_grad = tf.gradients(self.loss, tf.trainable_variables())
                        self.norm_grad = frobenius_norm(tf.concat([tf.reshape(g, [-1]) for g in self.var_grad], 0))

                        #session creation
                        config = tf.ConfigProto(allow_soft_placement = True)
                        config.gpu_options.allow_growth = True
                        self.session = tf.Session(config=config)

                        #session initialization
                        init = tf.global_variables_initializer()
                        self.session.run(init)

                        count_no_weights()

    def GAT_layer(self, X, Fout, idx_rows, idx_cols, A_shape, activation):
        X = tf.nn.dropout(X,  self.keep_prob)

        W = tf.get_variable("W", shape=[X.shape[1], Fout], initializer=tf.glorot_uniform_initializer())
        self.regularizers.append(tf.nn.l2_loss(W))
        X_w = tf.matmul(X, W)

        # simplest possible attention mechanism
        W_att1 = tf.get_variable("W_att1", shape=[X_w.shape[1], 1], initializer=tf.glorot_uniform_initializer())
        b_att1 = tf.get_variable("b_att1", shape=[1,], initializer=tf.zeros_initializer())
        self.regularizers.append(tf.nn.l2_loss(W_att1))
        W_att2 = tf.get_variable("W_att2", shape=[X_w.shape[1], 1], initializer=tf.glorot_uniform_initializer())
        b_att2 = tf.get_variable("b_att2", shape=[1,], initializer=tf.zeros_initializer())
        self.regularizers.append(tf.nn.l2_loss(W_att2))

        X_att_1 = tf.squeeze(tf.matmul(X_w, W_att1)) + b_att1
        X_att_2 = tf.squeeze(tf.matmul(X_w, W_att2)) + b_att2

        logits = tf.gather(X_att_1, idx_rows) +  tf.gather(X_att_2, idx_cols)

        A_att = tf.SparseTensor(indices=np.vstack([idx_rows, idx_cols]).T,
                                values=tf.nn.leaky_relu(logits),
                                dense_shape=A_shape)
        A_att = tf.sparse_reorder(A_att)
        A_att = tf.sparse_softmax(A_att)

        # apply dropout
        A_att = tf.SparseTensor(indices=A_att.indices,
                                values=tf.nn.dropout(A_att.values, self.keep_prob),
                                dense_shape=A_shape)
        A_att = tf.sparse_reorder(A_att)

        X_w = tf.nn.dropout(X_w, self.keep_prob)
        res = tf.sparse_tensor_dense_matmul(A_att, X_w)
        res = tf.contrib.layers.bias_add(res)

        return activation(res)

```

Multi-head Attention
^^^^^^^^^^^^^^^^^^^^

Analogous to multiple channels in ConvNet, GAT introduces **multi-head
attention** to enrich the model capacity and to stabilize the learning
process. Each attention head has its own parameters and their outputs can be
merged in two ways:

\begin{align}\text{concatenation}: h^{(l+1)}_{i} =||_{k=1}^{K}\sigma\left(\sum_{j\in \mathcal{N}(i)}\alpha_{ij}^{k}W^{k}h^{(l)}_{j}\right)\end{align}

or

\begin{align}\text{average}: h_{i}^{(l+1)}=\sigma\left(\frac{1}{K}\sum_{k=1}^{K}\sum_{j\in\mathcal{N}(i)}\alpha_{ij}^{k}W^{k}h^{(l)}_{j}\right)\end{align}

where $K$ is the number of heads. The authors suggest using
concatenation for intermediary layers and average for the final layer.

```python
#learning parameters and path dataset

learning_rate = 5e-3
val_test_interval = 1
num_hidden_feat = 8
n_heads = 8
gamma = 5e-4
patience = 100
path_dataset = './CORA/dataset.pickle'

#dataset loading
#ds = Dataset(path_dataset, normalize_feat=1)

A, X, Y, train_idx, val_idx, test_idx = process_data.load_data("cora", path_to_data=DATA_PATH)
X = process_data.preprocess_features(X)
```

    (2708, 2708)
    (2708, 1433)

```python
# extracts rows and cols of adjacency matrix
A = sp.csr_matrix(A)
A.setdiag(1)

idx_rows, idx_cols = A.nonzero()
```

```python
from tqdm import tqdm
```

```python
# num_exp = 10 #number of times training GCN over the given dataset
num_exp = 1 #number of times training GCN over the given dataset

list_all_acc = []
list_all_cost_val_avg  = []
list_all_data_cost_val_avg = []
list_all_acc_val_avg   = []
list_all_cost_test_avg = []
list_all_acc_test_avg  = []

num_done = 0
```

```python
num_total_iter_training = int(10e4)

GCNN = GAT(idx_rows, idx_cols, A.shape, X, Y, num_hidden_feat, n_heads, learning_rate=learning_rate, gamma=gamma)

cost_train_avg      = []
grad_norm_train_avg = []
acc_train_avg       = []
cost_test_avg       = []
grad_norm_test_avg  = []
acc_test_avg        = []
cost_val_avg        = []
data_cost_val_avg   = []
acc_val_avg         = []
iter_test           = []
list_training_time = list()

max_val_acc = 0
min_val_loss = np.inf

#Training code
for i in tqdm(range(num_total_iter_training)):
    if (len(cost_train_avg) % val_test_interval) == 0:
        #Print last training performance
        if (len(cost_train_avg)>0):
            tqdm.write("[TRN] epoch = %03i, cost = %3.2e, |grad| = %.2e, acc = %3.2e (%03.2fs)" % \
            (len(cost_train_avg), cost_train_avg[-1], grad_norm_train_avg[-1], acc_train_avg[-1], time.time() - tic))

        #Validate the model
        tic = time.time()

        feed_dict = {GCNN.idx_nodes: val_idx, GCNN.keep_prob:1.0}
        acc_val, cost_val, data_cost_val = GCNN.session.run([GCNN.accuracy, GCNN.loss, GCNN.data_loss], feed_dict)

        data_cost_val_avg.append(data_cost_val)
        cost_val_avg.append(cost_val)
        acc_val_avg.append(acc_val)
        tqdm.write("[VAL] epoch = %03i, data_cost = %3.2e, cost = %3.2e, acc = %3.2e (%03.2fs)" % \
            (len(cost_train_avg), data_cost_val_avg[-1], cost_val_avg[-1], acc_val_avg[-1],  time.time() - tic))

        #Test the model
        tic = time.time()

        feed_dict = {GCNN.idx_nodes: test_idx, GCNN.keep_prob:1.0}
        acc_test, cost_test = GCNN.session.run([GCNN.accuracy, GCNN.loss], feed_dict)

        cost_test_avg.append(cost_test)
        acc_test_avg.append(acc_test)
        tqdm.write("[TST] epoch = %03i, cost = %3.2e, acc = %3.2e (%03.2fs)" % \
            (len(cost_train_avg), cost_test_avg[-1], acc_test_avg[-1],  time.time() - tic))
        iter_test.append(len(cost_train_avg))

        if acc_val_avg[-1] >= max_val_acc or data_cost_val_avg[-1] <= min_val_loss:
            max_val_acc = np.maximum(acc_val_avg[-1], max_val_acc)
            min_val_loss = np.minimum(data_cost_val_avg[-1], min_val_loss)
            if acc_val_avg[-1] >= max_val_acc and data_cost_val_avg[-1] <= min_val_loss:
                best_model_test_acc = acc_test_avg[-1]
            curr_step = 0
        else:
            curr_step += 1
            if curr_step == patience:
                tqdm.write('Early stop! Min loss: ', min_val_loss, ', Max accuracy: ', max_val_acc)
                break

    tic = time.time()
    feed_dict = {GCNN.idx_nodes: train_idx, GCNN.keep_prob: 0.4}

    _, current_training_loss, norm_grad, current_acc_training = GCNN.session.run([GCNN.opt_step, GCNN.loss, GCNN.norm_grad, GCNN.accuracy], feed_dict)

    training_time = time.time() - tic

    cost_train_avg.append(current_training_loss)
    grad_norm_train_avg.append(norm_grad)
    acc_train_avg.append(current_acc_training)

#Compute and print statistics of the last realized experiment
list_all_acc.append(100*best_model_test_acc)
list_all_cost_val_avg.append(cost_val_avg)
list_all_data_cost_val_avg.append(data_cost_val_avg)
list_all_acc_val_avg.append(acc_val_avg)
list_all_cost_test_avg.append(cost_test_avg)
list_all_acc_test_avg.append(acc_test_avg)

print('Num done: %d' % num_done)
print('Max accuracy on test set achieved: %f%%' % np.max(np.asarray(acc_test_avg)*100))
print('Max suggested accuracy: %f%%' % (100*best_model_test_acc))#(np.asarray(acc_test_avg)[np.asarray(data_cost_val_avg)==np.min(data_cost_val_avg)]),))
print('Current mean: %f%%' % np.mean(list_all_acc))
print('Current std: %f' % np.std(list_all_acc))

num_done += 1
```

```python
#Print average performance
print(np.mean(list_all_acc))
print(np.std(list_all_acc))
```

    83.25999975204468
    0.5765412469708043

```python

```

Machine Learning Summer School 2019 (Moscow, Russia)/tutorials/graph_neural_networks/citation_networks/CORA_GAT.ipynb


# Optimización media-varianza

La **teoría de portafolios** es uno de los avances más importantes en las finanzas modernas e inversiones.
- Apareció por primera vez en un [artículo corto](https://www.math.ust.hk/~maykwok/courses/ma362/07F/markowitz_JF.pdf) llamado "Portfolio Selection" en la edición de Marzo de 1952 de "the Journal of Finance".
- Escrito por un desconocido estudiante de la Universidad de Chicago, llamado Harry Markowitz.
- Escrito corto (sólo 14 páginas), poco texto, fácil de entender, muchas gráficas y unas cuantas referencias.
- No se le prestó mucha atención hasta los 60s.

Finalmente, este trabajo se convirtió en una de las más grandes ideas en finanzas, y le dió a Markowitz el Premio Nobel casi 40 años después.
- Markowitz estaba incidentalmente interesado en los mercados de acciones e inversiones.
- Estaba más bien interesado en entender cómo las personas tomaban sus mejores decisiones cuando se enfrentaban con "trade-offs".
- Principio de conservación de la miseria. O, dirían los instructores de gimnasio: "no pain, no gain".
- Si queremos más de algo, tenemos que perder en algún otro lado.
- El estudio de este fenómeno era el que le atraía a Markowitz.

De manera que nadie se hace rico poniendo todo su dinero en la cuenta de ahorros. La única manera de esperar altos rendimientos es si se toma bastante riesgo. Sin embargo, riesgo significa también la posibilidad de perder, tanto como ganar.

Pero, ¿qué tanto riesgo es necesario?, y ¿hay alguna manera de minimizar el riesgo mientras se maximizan las ganancias?
- Markowitz básicamente cambió la manera en que los inversionistas pensamos acerca de esas preguntas.
- Alteró completamente la práctica de la administración de inversiones.
- Incluso el título de su artículo era innovador. Portafolio: una colección de activos en lugar de tener activos individuales.
- En ese tiempo, un portafolio se refería a una carpeta de piel.
- En el resto de este módulo, nos ocuparemos de la parte analítica de la teoría de portafolios, la cual puede ser resumida en dos frases:
 - No pain, no gain.
 - No ponga todo el blanquillo en una sola bolsa.

**Objetivos:**
- ¿Qué es la línea de asignación de capital?
- ¿Qué es el radio de Sharpe?
- ¿Cómo deberíamos asignar nuestro capital entre un activo riesgoso y un activo libre de riesgo?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Línea de asignación de capital

### 1.1. Motivación

El proceso de construcción de un portafolio tiene entonces los siguientes dos pasos:
1. Escoger un portafolio de activos riesgosos.
2. Decidir qué tanto de tu riqueza invertirás en el portafolio y qué tanto invertirás en activos libres de riesgo.

Al paso 2 lo llamamos **decisión de asignación de activos**.

Preguntas importantes:
1. ¿Qué es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
2. ¿Qué es la distribución óptima de activos?
 - ¿Cómo deberíamos distribuir nuestra riqueza entre el portafolo riesgoso óptimo y el activo libre de riesgo?
 - Concepto de **línea de asignación de capital**.
 - Concepto de **radio de Sharpe**.

Dos suposiciones importantes:
- Funciones de utilidad media-varianza.
- Inversionista averso al riesgo.

La idea sorprendente que saldrá de este análisis, es que cualquiera que sea la actitud del inversionista de cara al riesgo, el mejor portafolio de activos riesgosos es idéntico para todos los inversionistas.

Lo que nos importará a cada uno de nosotros en particular, es simplemente la desición óptima de asignación de activos.
___

### 1.2. Línea de asignación de capital

Sean:
- $r_s$ el rendimiento del activo riesgoso,
- $r_f$ el rendimiento libre de riesgo, y
- $w$ la fracción invertida en el activo riesgoso.

 Realizar deducción de la línea de asignación de capital en el tablero.

**Tres doritos después...**

#### Línea de asignación de capital (LAC):
$E[r_p]$ se relaciona con $\sigma_p$ de manera afín. Es decir, mediante la ecuación de una recta:

$$E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p.$$

- La pendiente de la LAC es el radio de Sharpe $\frac{E[r_s-r_f]}{\sigma_s}=\frac{E[r_s]-r_f}{\sigma_s}$,
- el cual nos dice qué tanto rendimiento obtenemos por unidad de riesgo asumido en la tenencia del activo (portafolio) riesgoso.

Ahora, la pregunta es, ¿dónde sobre esta línea queremos estar?
___

### 1.3. Resolviendo para la asignación óptima de capital

Recapitulando de la clase pasada, tenemos las curvas de indiferencia: **queremos estar en la curva de indiferencia más alta posible, que sea tangente a la LAC**.

 Ver en el tablero.

Analíticamente, el problema es

$$\max_{w} \quad E[U(r_p)]\equiv\max_{w} \quad E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde los puntos $(\sigma_p,E[r_p])$ se restringen a estar en la LAC, esto es $E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p$ y $\sigma_p=w\sigma_s$. Entonces el problema anterior se puede escribir de la siguiente manera:

$$\max_{w} \quad r_f+wE[r_s-r_f]-\frac{1}{2}\gamma w^2\sigma_s^2.$$

 Encontrar la $w$ que maximiza la anterior expresión en el tablero.

**Tres doritos después...**

La solución es entonces:

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}.$$

De manera intuitiva:
- $w^\ast\propto E[r_s-r_f]$: a más exceso de rendimiento que se obtenga del activo riesgoso, más querremos invertir en él.
- $w^\ast\propto \frac{1}{\gamma}$: mientras más averso al riesgo seas, menos querrás invertir en el activo riesgoso.
- $w^\ast\propto \frac{1}{\sigma_s^2}$: mientras más riesgoso sea el activo, menos querrás invertir en él.
___

## 2. Ejemplo de asignación óptima de capital: acciones y billetes de EU

Pongamos algunos números con algunos datos, para ilustrar la derivación que acabamos de hacer.

En este caso, consideraremos:
- **Portafolio riesgoso**: mercado de acciones de EU (representados en algún índice de mercado como el S&P500).
- **Activo libre de riesgo**: billetes del departamento de tesorería de EU (T-bills).

Tenemos los siguientes datos:

$$E[r_{US}]=11.9\%,\quad \sigma_{US}=19.15\%, \quad r_f=1\%.$$

Recordamos que podemos escribir la expresión de la LAC como:

\begin{align}
E[r_p]&=r_f+\left[\frac{E[r_{US}-r_f]}{\sigma_{US}}\right]\sigma_p\\
      &=0.01+\text{S.R.}\sigma_p,
\end{align}

donde $\text{S.R}=\frac{0.119-0.01}{0.1915}\approx0.569$ es el radio de Sharpe (¿qué es lo que es esto?).

Grafiquemos la LAC con estos datos reales:

```python
# Importamos librerías que vamos a utilizar
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Datos
Ers = 0.119
ss = 0.1915
rf = 0.01
# Radio de Sharpe para este activo
SR = (Ers - rf) / ss
# Vector de volatilidades del portafolio (sugerido: 0% a 50%)
sp = np.linspace(0, 0.5, 100)
# LAC
Erp = rf + SR * sp
```

```python
# Gráfica
plt.figure(figsize=(6, 4))
plt.plot(sp, Erp, lw=3, label='LAC')
plt.plot(0, rf, 'ob', ms=5, label='Libre de riesgo')
plt.plot(ss, Ers, 'or', ms=5, label='Portafolio/activo riesgoso')
plt.legend(loc='best')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
```

Bueno, y ¿en qué punto de esta línea querríamos estar?
- Pues ya vimos que depende de tus preferencias.
- En particular, de tu actitud de cara al riesgo, medido por tu coeficiente de aversión al riesgo.

Solución al problema de asignación óptima de capital:

$$\max_{w} \quad E[U(r_p)]$$

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}$$

Dado que ya tenemos datos, podemos intentar para varios coeficientes de aversión al riesgo:

```python
# importar pandas
import pandas as pd
```

```python
# Crear un DataFrame con los pesos, rendimiento
# esperado y volatilidad del portafolio óptimo
# entre los activos riesgoso y libre de riesgo
# cuyo índice sean los coeficientes de aversión
# al riesgo del 1 al 10 (enteros)
gamma = np.arange(1, 11)
w = (Ers - rf) / (gamma * ss**2)
pd.DataFrame({'$\gamma$': gamma, '$w$': w})
```

      $\gamma$
      $w$

      0
      1
      2.972275

      1
      2
      1.486137

      2
      3
      0.990758

      3
      4
      0.743069

      4
      5
      0.594455

      5
      6
      0.495379

      6
      7
      0.424611

      7
      8
      0.371534

      8
      9
      0.330253

      9
      10
      0.297227

¿Cómo se interpreta $w^\ast>1$?
- Cuando $0<w^\ast<1$, entonces $0<1-w^\ast<1$. Lo cual implica posiciones largas en el mercado de activos y en el activo libre de riesgo.
- Por el contrario, cuando $w^\ast>1$, tenemos $1-w^\ast<0$. Lo anterior implica una posición corta en el activo libre de riesgo (suponiendo que se puede) y una posición larga (de más del 100%) en el mercado de activos: apalancamiento.

# Anuncios parroquiales.

## 1. Quiz la siguiente clase.

## 2. Pueden consultar sus calificaciones en el siguiente [enlace](https://docs.google.com/spreadsheets/d/1qDJGx731VGLk_LlxrYibolp4UwIMMNCsiQDTnm4QWew/edit?usp=sharing)

Created with Jupyter by Esteban Jiménez Rodríguez.


# Background

This post will introduce data scientists who are interested in cryptocurrency exchange models that can be used to predict buy and sell opportunities based on several strategies with in depth descriptions and access to code that is being used to simulate a variety of models with varying performance.

## What is Forex

Forex is short for foreign currency exchange and is the trading of currencies with the goal making profits by timing the buy and sell of specific currency pairs while using candlestick charts. Strategies for trading are created by looking for patterns that can be used to predict future currency exchange price fluctuations.

## Candlestick Charts

A candlestick chart is the standard plot used in visualizing trading activity where a candle is represented by a box plot that visualizes 4 prices within a given period: the high, low, open and close price.  The box, or body of the candle, is colored based on if the open price is greater than the close and differently if vice versa.  In the below chart, a white candlestick means the close price is higher than the open price meaning the price is going up.  The lines coming out of the candlestick body are called "shadows" or "wicks" and represent the price spread for the given period by extending out to the high and low price.  An individual candlestick can represent a period as short as a second to days or weeks or more.  The chart below is a 15 minute candlestick chart so each candlestick represents a 15 minute period.

Figure X - Here is an example 15-minute candlestick chart for the Ethereum/Bitcoin cryptocurrency exchange rate.
This visualization was rendered using the Python library mplfinance.

# Feature Engineering

## Lookbacks

Each record in the dataset represents a single candlestick's data.  Since context likely matters, a lookback feature is the candlestick data for a previous candlestick included with the current record.  A lookback can be for any feature already in the dataset.  For example, if a candlestick data is represented just by `open`, `close`, `high`, `low` price data.  A lookback of 1 could be something like this for a 2-record dataset:

|index|open|close|high|low|open_1|close_1|high_1|low_1|
|:-|-:|-:|-:|-:|-:|-:|-:|-:|
|2021-07-29 01:00:00|0.057756|0.057657|0.057776|0.057576|--|--|--|--|
|2021-07-29 01:00:00|0.057657|0.057697|0.057788|0.057612|0.057756|0.057657|0.057776|0.057576|

## Indicators

Indicators are non-Boolean numerical features that are calculated based on a window of data.

### Moving Averages (MA)

Moving averages are common indicators used in forex and can give insight into price trend or momentum by smoothing out the variations.  A moving average operates on a window which represents the average over a given number of records.  A typical value of `14` is used in forex and for this work three are used: `14`, `30` and `90`.

The equation for moving average is the following where *k* is the window size on a dataset containing *N* items:

### Average True Range (ATR)

The ATR is an indicator of volatility in the market.  A higher ATR and the price is more likely to jump around which can result in higher gains but at higher risk.  It is calculated by looking at the true range (TR) of the current record and then averaging out over a window typically of size `14`.  This is also useful determining target and stop loss prices which will be discussed in the different strategies.

### Relative Strength Index (RSI)

The RSI is a momentum indicator that measures the magnitude of recent price changes.  The value is limited between `0` and `100`.  Traditionally, if the RSI value is above `70`, it is considered "overvalued" and the price may soon reverse.  A value below `30` is thus considered "undervalued".  The calculation for this is more complicated.  First the relative strength is found by comparing the exponential moving averages of the positive differences in closing price divided by the exponential moving averages of the negative differences in the closing price.  Then the value is normalized into an index between `0` and `100`.

### Support and Resistance Lines

Support and resistance are levels that act as barriers that prevent a price from either dropping below (support) or raising above (resistance) an expected price.  The levels can be horizontal or they can represent a trendline.  While there is no agreed formula, this feature dataset will use the `min` and `max`  in a given window.  Again, three windows will be used resulting in three features `14`, `30` and `90`.

## Signals

Signals are Boolean features represented by either a `1` (True) or `0` (False).

### Upward Trend

Is the current record within the context of an upward trend?  To calculate this, the difference in moving average is used for a given window.

### Candlestick Patterns

There are numerous candlestick patterns and each pattern tells a story of how the market is behaving and what that might mean for the future.  Their names often derive from their shape and the table below provides links to more information about the ones supported in this dataset.  The candlestick pattern signals are implemented according to the below algorithms.

|Pattern Name|Indication|Image|Algorithm|
|:-|:-|:-:|:-|
|Shooting Star|Bearish Reversal|| |
|Hammer|Bullish Reversal||  |
|Bearish Harami|Bearish Reversal|| |
|Bullish Harami|Bullish Reversal|| |
|Engulfing Bullish|Bullish Reversal|| |
|Engulfing Bearish|Bearish Reversal|| |

## Other Features

### Date and Time

It's possible that certain days-of-the-week (DOW) or time-of-the-day (TOD) impact trading trends.  For this reason, DOW and TOD are added as features in a numerical format based on the candlestick's open time.  The DOW is represented by a number between `0` and `6` and the TOD is normalized between `0` and `1`.

### Trading Volume

Within the candlestick data that is coming from Binance there are five features that relate to trading volume.  This data is included, unaltered, as available features.  They include `number_of_trades`, `volume`, `quote_asset_volume`, `taker_buy_base_asset_volume`, and `taker_buy_quote_asset_volume`.

\begin{align}
\text{MA} &= \frac{1}{k}\sum_{i=n-k+1}^{N} p_{i}
\end{align}

\begin{align}
\text{TR} &= \text{max}\left[\left(\text{high}-\text{low}\right), \text{abs}\left(\text{high}-\text{close}_{\text{prev}}\right), \text{abs}\left(\text{low}-\text{close}_{\text{prev}}\right)\right]
\\
&= \text{max}\left[\left(\text{high}, \text{close}_{\text{prev}}\right)\right] - \text{min}\left[\left(\text{low}, \text{close}_{\text{prev}}\right)\right]
\\
\text{ATR} &= \frac{1}{k}\sum_{i=n-k+1}^{N} \text{TR}_{i}
\end{align}

\begin{align}
U &= \begin{cases}
\text{close}-\text{close}_{\text{prev}} & \text{if close}_{\text{prev}} < \text{close} \\
0 & \text{if close}_{\text{prev}} \geq \text{close}
\end{cases}
\\
\\
D &= \begin{cases}
\text{close}_{\text{prev}}-\text{close} & \text{if close}_{\text{prev}} > \text{close} \\
0 & \text{if close}_{\text{prev}} \leq \text{close}
\end{cases}
\\
\\
\text{EMA}_{i} &= \left[p_{i} \times \left(\frac{s}{1+d}\right)\right] + \text{EMA}_{i-1} \times \left[1-\left(\frac{s}{1+d}\right)\right]
\\
\text{RS} &= \frac{\text{EMA}_{U}}{\text{EMA}_{D}}
\\
\text{RSI} &= 100-\frac{100}{1+RS}
\end{align}

\begin{align}
\text{support} &= \text{min}\left[\text{low}_{n-k+1}, \text{low}_{n-k+2} \ldots, \text{low}_{n}\right]
\\
\text{resistence} &= \text{max}\left[\text{high}_{n-k+1}, \text{high}_{n-k+2} \ldots, \text{high}_{n}\right]
\end{align}

\begin{align}
t_{j} &= \text{MA}_{j} - \text{MA}_{j-1}
\\
r_{j} &= \begin{cases}
1 & \text{if } t_{j} > 0 \\
0 & \text{if } t_{j} \leq 0 \\
\end{cases}
\end{align}

\begin{align}
r_{\text{ss}} &= \begin{cases}
1 & \text{if}  \text{ low} + 0.382\left(\text{high}-\text{low}\right) \geq \text{close } \tiny{\text{and}} \\
  & \hspace{0.45cm} \text{open } \leq \text{close} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{ssr}} &= \begin{cases}
1 & \text{if }  \text{low} + 0.382\left(\text{high}-\text{low}\right) \geq \text{open } \tiny{\text{and}} \\
  & \hspace{0.45cm} \text{open } > \text{close} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{hm}} &= \begin{cases}
1 & \text{if }  \text{high} - 0.382\left(\text{high}-\text{low}\right) \leq \text{close } \tiny{\text{and}} \\
  & \hspace{0.45cm} \text{open } \geq \text{close} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{hmr}} &= \begin{cases}
1 & \text{if }  \text{high} - 0.382\left(\text{high}-\text{low}\right) \leq \text{open } \tiny{\text{and}} \\
  & \hspace{0.45cm} \text{open} < \text{close} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{brh}} &= \begin{cases}
1 & \text{if } \text{open} > \text{close}                               \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} < \text{close}_{\text{prev}} \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{close}_{\text{prev}} \geq \text{open }           \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} \leq \text{close}            \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{high}_{\text{prev}} \geq \text{high}             \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{low}_{\text{prev}} \leq \text{low} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{buh}} &= \begin{cases}
1 & \text{if } \text{open} < \text{close}                               \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} > \text{close}_{\text{prev}} \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{close}_{\text{prev}} \leq \text{open }           \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} \geq \text{close}            \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{high}_{\text{prev}} \geq \text{high}             \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{low}_{\text{prev}} \leq \text{low} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{ebu}} &= \begin{cases}
1 & \text{if } \text{open} < \text{close}                               \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} > \text{close}_{\text{prev}} \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{close}_{\text{prev}} \geq \text{open }           \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} \leq \text{close}            \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{high}_{\text{prev}} \leq \text{high}             \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{low}_{\text{prev}} \geq \text{low} \\
0 & \text{else} \\
\end{cases}
\end{align}

\begin{align}
r_{\text{ebr}} &= \begin{cases}
1 & \text{if } \text{open} > \text{close}                               \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} < \text{close}_{\text{prev}} \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{close}_{\text{prev}} \leq \text{open }           \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{open}_{\text{prev}} \geq \text{close}            \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{high}_{\text{prev}} \leq \text{high}             \hspace{0.2cm}\tiny{\text{and}} \\
  &\hspace{0.4cm}\text{low}_{\text{prev}} \geq \text{low} \\
0 & \text{else} \\
\end{cases}
\end{align}

# Strategies

## Target and Stop Loss

In traditional forex trading, stop and limit orders are methods to protect an investor that can be used to buy and sell currencies when a price reaches a certain level.  Using this, a predictive model can focus only on buy opportunities and then rely on a simple strategy to determine when to sell. A sell strategy defines two sell prices for a given buy opportunity.  The first sell price is called the **target**, which is the high price that results in a profit and the next is the **stop loss**, which is the low resulting in a loss. When a buy opportunity is identified, and a target and stop loss is calculated, the purchase can be made and the sell will be automatic either by the exchange or by another system that monitors the market price.

In the example below, a buy opportunity is identified at the close of the 4:00am candlestick at a price of `0.060497` Bitcoin (`BTC`) per 1.0 Etherum (`ETH`).  Buying ETH at this price, a target and stop loss is calculated with a `1.0% : 0.5%` ratio, thus `0.061102` for a target and `0.060195` for a stop loss.  The price reaches the target price eight candlesticks later or 2 hours later at 6:00am, thus securing `1.0%` profit (assuming no fees).

Figure X - Example ETH buy opportunity.

### Identifying Buying Opportunities

Using a target and stop loss approach simplifies the model to a binary classification problem but a new problem is created, there is no labeled data to train on.  The goal here is to create a label for each record.  A record being the data for one candlestick.  This includes the low, high, open and close prices as well as some additional features such as volume and number of trades.  Using the close price for each record, a target and stop loss price is calculated using the same threshold ratio that will be used on the deployed model.  Using the example above, a ratio of `1.0% : 0.5%` returns a target price `1.0%` higher than the close price and a stop loss of `0.5%` below the close price.  The next step is to peek into the future and see what happens first. Does the price reach the target price first or the stop loss?  If it reaches the target, the record's label will be a `1` meaning "buy".  Another consideration is how far in the future it should look.  This is called the "window".  Typically, 15 candles in the future is used.  If the price reaches stop loss first or if price hovers between the target and stop loss within the window, the record will be a `0` meaning "not buy".

A common question is why not make the stop loss as small as possible?  Setting the stop loss too small can result in being "wicked out" of a trade.  Looking at figure above, if the stoploss is made too small, the wick of the next candle after the buy could poke through resulting in the stop loss being breached before the target price, thus resulting in a "not buy".  Therefore, setting a higher stop loss gives some buffer for the price to fluctuate before a gain is achieved while minimizing losses.

For the remainder of the target stop loss strategy discussion, the strategy will focus on `BTC` buy opportunities with the starting coin being `ETH`.  In other words, `ETH` will be used to buy `BTC` and will be sold back to `ETH` when the price reaches a target or stop loss price.  This can cause a bit of confusion because the price is the number of BTC within 1 ETH which means, a profit is made when the price actually drops thus the target will be lower than stop loss (opposite the figure above).  It is for this reason the `reverse` flag is set to `True` (see below).

### Determining Ideal Ratios

In the example above, a `1.0% : 0.5%` ratio is used but is this a good ratio to use?  Setting a ratio of `10% : 5%` might be too high because it would be unlikely to gain `10%` resulting in a very sparsely labeled dataset.  Likewise, using a ratio of `0.1% : 0.005%` could be too low, especially when considering transaction fees (to be discussed later).  It's also worth mentioning that using a percentage might result in inconsistencies since some currency pairs are more volatile than others and volatility for a given pair can change over time.  For this reason, forex traders sometimes use a ratio of the ATR.  For example, using an ATR `2:1` ratio is a good place to start.

Models generally perform better on balanced data so getting half of the labels to be `1` is ideal.  But achieving this with a ratio that is consistent and profitable may not be practical.  To find a good ratio, different multiples are generated and the percent of `1`'s is plotted.  On the below ATR ratio figure, the multiple of `2x` means the numerator is `2` times the denominator, where the denominator is the `x-axis` value.  Therefore, when `x-axis = 3` the ratio is `6:3`.  When the multiple is `4x` and `x-axis = 2`, the ratio is `8:2`, etc.  For the percentage ratio, `x-axis` represents the numerator, and the denominator is then the numerator divided by the legend's label.  For example, when `x-axis = 0.01` for the `/2` line, the ratio is `1.0% : 0.5%`.

Figure X - Finding the best ratios to maximize label data using a window of 30 on ETHBTC 15-minute candles.

Unsurprisingly, as the ratio grows or ratio multiple grows, fewer buy opportunities can be found in the data because there are fewer windows where high profits can be achieved and fewer windows where smaller stop losses don't get wicked out by the volatility of the market.  None of the plots reaches the goal of `50%` but the results provide plenty of options to avoid sparsely labeled data.  From this analysis, the ATR Ratio is maximized at `2:1` with approximately `40%` of the labels being `1`.  The percentage ratio is maximized at `1.0% : 0.5%` with approximately `27%` of the labels being `1`.

### Building Xy Datasets

The imbalance of a dataset is not the only criteria for determining if one ratio is better than another, but it does give a sense.  To explore this further, several labeled datasets are generated with different labelling strategies by changing ratios, the window, and the whether the ratio represents a percentage or ATR ratio.  The following table shows 12 different labeled datasets generated that are used to compare different model performances.

|dataset|use_atr|ratio|reverse|window|size|true_labels|imbalance|train_imbal|test_imbal|
|:-|:-:|:-:|:-:|:-:|-:|-:|-:|-:|-:|
|20210806a|False|(0.01, 0.005)|True|30|141174|37979|0.269023|0.262033|0.319201|
|20210806b|False|(0.01, 0.0025)|True|30|141174|23782|0.168459|0.169634|0.196954|
|20210806c|False|(0.0075, 0.0025)|True|30|141174|29824|0.211257|0.220094|0.233808|
|20210806d|True|(2, 1)|True|15|141174|44769|0.317119|0.317522|0.341973|
|20210806e|True|(4, 2)|True|15|141174|17024|0.120589|0.123430|0.124245|
|20210806f|True|(4, 1)|True|15|141174|15304|0.108405|0.110710|0.111111|
|20210806g|True|(4, 2)|True|30|141174|31640|0.224121|0.227442|0.238302|
|20210806h|True|(4, 1)|True|30|141174|26488|0.187627|0.189435|0.201498|
|20210806i|True|(2, 1)|True|30|141174|55315|0.391821|0.391990|0.409738|
|20210806j|False|(0.01, 0.005)|True|15|141174|29035|0.205668|0.187259|0.265218|
|20210806k|False|(0.01, 0.0025)|True|15|141174|19184|0.135889|0.129528|0.174831|
|20210806l|False|(0.0075, 0.0025)|True|15|141174|25995|0.184134|0.183938|0.224619|

It is during this time that feature engineering was initially performed but later not necessary since the AWS pipeline has already completed this step (discussed later).  For this reason, each dataset already includes all the features discussed in the feature engineering section plus `14` lookbacks resulting in `542` features (a lookback is the previous records features).

Each dataset is also split into train and validation sets according to the table below.

|Purpose|Start Date|End Date|Number of Records|
|:-|:-:|:-:|-:|
|*not used*|--|2017/12/31|16353|
|Train|2018/01/01|2020/12/31|104796|
|Validation|2021/01/01|2021/07/29|20025|
|Test|2021/07/30|--|--|

In 2017, Binance started reporting figures and it took some time for these to develop robustness.  For this reason, 2017 is excluded from the training and testing sets.  The final test set analysis is performed on AWS in simulation using near-live data.

### Simulating Trades on Labeled Data

To get a sense of what an ideal profit would look like for each of the labelling strategies, it is necessary to run them through a simulator.  Why is this necessary?  Why can't this be calculated from the above figures?  To answer this, imagine having two candles, one after another, where the labeling has marked both as `1`.  In a deployed model, when a buy signal is received, all available currency will be spent.  When evaluating the next candlestick data, the model will be evaluating for selling, not buying, so that candlestick will not be evaluated for a buy opportunity.

The simulator logic is the same logic as in the deployed model pipeline but instead of looking at the predictions, it looks at the validation labeled data which is already guaranteed to contain profitable trades assuming it uses the same ratio and other hyperparameters as the dataset's labelling strategy.  The simulator works, in short, by progressing through the labeled data, looking for the next buy opportunity, calculates the target and stop loss prices, finds the next record that surpasses one of these, calculating the profit/loss along with any fees, and then repeats the process until it reaches the end.  The last sell indicates the maximized profit achievable for the dataset.  The below table shows how each dataset's number of trades and maximized profit based on a starting value of `1 ETH` and a fee of `0.1%` for each buy or sell transaction using the validation data.

|dataset|sim_num_trades|sim_max_profit|sim_bad_trades|
|:-|-:|-:|-:|
|20210806a|1179.0|13228.993600|0.0|
|20210806b|1093.0|6620.481356|0.0|
|20210806c|1456.0|3126.592203|0.0|
|20210806d|1128.0|17342.974458|1.0|
|20210806e|335.0|416.560613|0.0|
|20210806f|340.0|467.521090|1.0|
|20210806g|368.0|1084.998178|0.0|
|20210806h|376.0|1295.216356|2.0|
|20210806i|1033.0|9843.078247|2.0|
|20210806j|1199.0|15539.693313|0.0|
|20210806k|1097.0|6837.112001|0.0|
|20210806l|1497.0|3921.842442|0.0|

While the label data guarantees a trade is profitable, it doesn't guarantee the profit surpasses the fee amount.  For this reason, some labels result in bad trades.  In the above table, only the ATR ratio datasets result in bad trades which makes sense since all the percentage-based datasets are larger than the fee.  Keeping an eye on this number will be important when using an ATR ratio dataset.

### Comparing Datasets with Base Classifiers

For each of the datasets, a set of base classifiers are trained.  Below is a table of the base classifiers used.

|Name|Parameters|
|:-|:-|
|GaussianNB|*none*|
|LogisticRegression|`random_state=42, max_iter=10000`|
|RandomForestClassifier|`random_state=42, n_jobs=-1`|
|AdaBoostClassifier|`random_state=42`|
|GradientBoostingClassifier|`random_state=42`|
|XGBClassifier|`n_jobs=-1, random_state=42, use_label_encoder=False`|
|MLPClassifier|`random_state=42`|

One note about the `MLPClassifier`: since this classifier is sensitive to scaling, `make_pipeline()` with `StandardScaler()` is used.

This exercise determines the F1-score, precision, recall and the simulator's profit on the validation set for each dataset/classifier combination resulting in `84` results.  This was performed two more times on the same datasets and classifiers but first reducing the number of lookbacks from `14` to `3` and then to `0` thus reducing the number of features each time and ending up with a total of 252 trained model results.

### Identifying Best Performing Dataset/Classifier Combinations

When it comes to ranking best performance, precision is a good starting point.  A high precision means the model has reduced the number of false positives (FP).  In other words, it reduced the chance of predicting a buy that is unprofitable--the absolute worst case that should be avoided.  A low recall, on the other hand, just means the model is predicting fewer buy opportunities than expected.  This is generally fine so long as it does predict buys often enough (one true positive every couple of days on average).

Looking at precision alone can be misleading.  On the extreme side, a precision of `1.0` is perfect precision but if recall was very low, such as having only one `1` true positive (TP), the model would be ineffective since it so rarely makes predictions.  For this reason, the F1-score is not a good show of performance, and several factors must be considered.  Maximizing precision and the number of TPs is the overall goal.  Ranking based on the number TPs does little in the way of explaining performance if the number of FPs is still high. Therefore, ranking is performed first on precision, second on the difference between TPs and FPs and then on the ratio between. The top 10 models based on this ranking is shown in the table below.

|Rank|Classifier|Dataset|Lookbacks|TP|FP|Diff|Ratio|Precision|Recall|Sim. Profit|
|-:|:-|:-|-:|-:|-:|-:|-:|-:|-:|-:|
|1|LogisticRegression|20210806i|0|453|277|176|1.64|0.6204|0.0082|1.3210|
|2|AdaBoostClassifier|20210806g|14|98|13|85|7.54|0.8824|0.0031|1.0243|
|3|LogisticRegression|20210806i|3|840|631|209|1.33|0.5708|0.0152|1.6651|
|4|RandomForestClassifier|20210806g|14|2075|1856|219|1.12|0.5278|0.0656|1.2348|
|5|LogisticRegression|20210806i|14|1731|1535|196|1.13|0.5299|0.0313|1.0503|
|6|LogisticRegression|20210806d|0|40|26|14|1.54|0.6000|0.0009|1.0045|
|7|GradientBoostingClassifier|20210806d|0|58|38|20|1.53|0.6000|0.0013|1.0126|
|8|GradientBoostingClassifier|20210806i|3|2710|2460|250|1.10|0.5241|0.0490|0.6239|
|9|GradientBoostingClassifier|20210806i|14|1919|1770|149|1.08|0.5201|0.0347|0.8400|
|10|AdaBoostClassifier|20210806a|14|60|54|6|1.11|0.5263|0.0016|0.9951|

Reviewing the simulated profit for each of these, the top 7 all produce profits suggesting the ranking is robust.  The highest performing models have then been deployed to AWS for live simulations which is discussed below.

### Building a Logistic Regression Ensemble

Many logistic regression models outperformed other models and are easy to train and tune so it seems logical to ask if performance could be improved further with an ensemble.  To build an ensemble, the prediction from each model in the ensemble is weighted and summed and if that sum is greater than or equal to some threshold, the prediction would be considered a `1` or else `0`.  The weights will be the precision of each model on the validation set.  The equation for this can be explained as follows:

Where *r_j* is the prediction result for the *j*th record, *p_m[i]* is the prediction of *i*th model in *m*, *c_m[i]* is the precision of said model, and *t* is the hyperparameter threshold.  Any model that has a validation set precision of `0` would always be zeroed out so these models will not be included in the ensemble.

Finding a good value for *t* can be achieved by trying out by measuring the precision, recall and simulated profit.  Another issue that needs to be considered is that the datasets use different ratios so which ratio should be used on the ensemble?  It stands to reason that the model/dataset with the highest precision should be used since that carries the most weight.  However, simulations show this is not always the case as will be shown later with the scaled version shown later.  For the logistic regression, it so happens that these are aligned.  In the below figure, profit is maximized at threshold of `0.12` with a value of `1.86` which surpasses any individual model simulation performance.

Figure X - Simulated precision, recall and profit for varying thresholds on a Logistic Regression ensemble.

### Scaling Data in Isolation

There is a forex theory that a good strategy is generalizable, in that it can be applied to any currency pair, even opposite pairs, and be profitable.  All models previously train (except for the MLP) have not been scaled so it is impractical to expect one of these models perform well for both ETH to BTC and the opposite BTC to ETH.  Likewise, using standard scaling, like done for the MLP, is also not practical since the dataset for BTC to ETH trades is scaled very differently.  So, can the data be scaled in isolation?  The answer is yes, but at the cost of zeroing-out one of the features.  By defining the open price as the mean and using the close, high, and low in the calculation of a standard deviation, all price data can be scaled such that one standard deviation difference is -1 or 1.  The formula can be described as the following:

Using this scaling algorithm, each record is individually scaled independent of the other data in the dataset.  Repeating the same model/dataset comparison using this scaler produces another 252 trained models.  Many of the logistic regression models returned profits in simulation of `ETHBTC` suggesting again an ensemble might outperform a single model.

### Ensemble with Custom Scaling

Using the scaler discussed previously, a new ensemble of logistic regression models can be produced with the goal of having a model that be able to perform on both `ETHBTC` and `BTCETH` trading.  Again, a threshold and ratio are brute forced.  In the below figure, profit is maximized at threshold of `0.10` with a value of `1.35` again surpassing any individual model simulation performance.  This time, the profit is maximized with a ratio of `4:2` in contrast with the highest precision dataset being `2:1`.

Figure X - Simulated precision, recall and profit for varying thresholds on a scaled Logistic Regression ensemble.

### Deep Neural Network

To see how a deep neural network can perform on the same datasets used to train base classifiers, the dataset `20210806i` is chosen for its consistent performance.  A variety of ResNet28 models are built using PyTorch and trained with varying hyperparameters or model changes over 20 epochs on a GPU to allow for quicker iteration.  A subset of these run configurations is shown in the table below.

|Model Name|Scaler|Width|Optimizer|Learning Rate|
|:-|:-|-:|:-|:-|
|nm_torch1_alpha33|CustomScaler1|128|AdamW|0.006|
|nm_torch1_alpha34|*none*|128|AdamW|0.006|
|nm_torch1_alpha35|*none*|32|AdamW|0.001|
|nm_torch1_alpha36|*none*|32|AdamW|0.006|
|nm_torch1_alpha37|CustomScaler1|32|AdamW|0.006|
|nm_torch1_alpha38|CustomScaler1|128|AdamW|0.03|

Below is a figure of the training and validations results of this subset of models.  While 20 epochs is still quite young for a comprehensive analysis, some trends do start to appear which only become more pronounced with further epochs and as many as 100 when the training set begins to converge.

Figure X - Simulated precision, recall and profit for varying thresholds on a scaled Logistic Regression ensemble.

The general trend that is consistent across all models is that the training loss drops predictably and consistently but the validation loss steeply rises after just a few epochs.  The recall on the validation set generally does slowly improve which also improves the F1-score but the precision remains erratic, averaging around `0.5`.  This unexpected behavior is likely due to a shift in the how the `ETHBTC` market behaves over time, so the model is learning a strategy that is no longer profitable in 2021.  To validate this, each model was simulated with the results in the below table.

|Model Name|Buys|Starting Value|Ending Value|
|:-|-:|:-:|:-|
|nm_torch1_alpha33|3989|1.0|0.3472|
|nm_torch1_alpha34|0|1.0|0.0|
|nm_torch1_alpha35|51|1.0|1.0391|
|nm_torch1_alpha36|0|1.0|0.0|
|nm_torch1_alpha37|3083|1.0|0.4240|
|nm_torch1_alpha38|236|1.0|0.9454|

### Ensemble
\begin{align}
x_{j} &= \sum_{i=0}^M \left( p_{m[i]} \cdot c_{m[i]}\right)
\\
r_{j} &= \begin{cases}
1 & \text{if } x_{j} \geq t \\
0 & \text{if } x_{j} < t \\
\end{cases}
\end{align}

### Scaler

\begin{align}
\mu_{j} &= p_{j}^\text{open}
\\
\sigma_{j} &= \sqrt{\left(p_{j}^\text{close} - p_{j}^\text{open}\right)^2 + \left(p_{j}^\text{high} - p_{j}^\text{open}\right)^2 + \left(p_{j}^\text{low} - p_{j}^\text{open}\right)^2}
\\
p_{j} &= \begin{cases}
\frac{\left(p_{j} - \mu_{j}\right)}{\sigma_{j}} & \text{if } p_{j} \text{ is a price} \\
\frac{p_{j}}{p_{j}^\text{ATR}} & \text{if } p_{j} \text{ is an ATR difference} \\
\frac{p_{j} - p_{j}^\text{ATR}}{p_{j}^\text{ATR}} & \text{if } p_{j} \text{ is an ATR lookback} \\
\frac{p_{j}}{p_{j}^\text{RSI}} & \text{if } p_{j} \text{ is an RSI difference} \\
\frac{p_{j} - 50}{20} & \text{if } p_{j} \text{ is an RSI or RSI lookback} \\
\end{cases}
\end{align}

Converted to images with [LaTeX to Image converter](https://latex2image.joeraut.com/)


```python
from sympy import symbols,Matrix,init_printing
init_printing()
```

```python
a,b,c,d,e,f,g,h,i = symbols('a,b,c,d,e,f,g,h,i')
```

```python
R = Matrix([[a,b,c],[d,e,f],[g,h,i]])
R
```

$$\left[\begin{matrix}a & b & c\\d & e & f\\g & h & i\end{matrix}\right]$$

```python

```