R оптимизация купить продать

Мне нужно найти решение проблемы оптимизации. В моем упрощенном примере у меня есть прогноз цен на следующий год. У меня есть инвентарь, который может содержать не более 25 продуктов. Я могу продавать или покупать каждый месяц. Я не могу купить более 4 товаров или продать более 8 товаров в месяц. Я ищу прибыль, покупая по более низкой цене, чем продавая. Есть ли пакет / функция, которая может указать, когда покупать, а когда продавать? Цель состоит в том, чтобы максимизировать прибыль в конце периода при сохранении установленных условий (см. Пример ниже). Также предлагается возможное ручное решение. В реальном приложении будут дополнительные условия, например, что мне нужно поддерживать определенный уровень запасов зимой или что максимальная покупка / продажа зависит от уровня запасов. Например. если запасы высоки, вы можете продать больше и т. д.

library(tidyverse)
library(lubridate)

df <- tibble(
  date = ymd("2020-06-01") + months(0:11),
  price = c(12, 11, 12, 13, 16, 17, 18, 17, 18, 16, 17, 13),
  total_capacity = 25,
  max_units_buy = 4,
  max_units_sell = 8)

# date             price          total_capacity max_units_buy  max_units_sell
#  1 2020-06-01    12             25             4              8
#  2 2020-07-01    11             25             4              8
#  3 2020-08-01    12             25             4              8
#  4 2020-09-01    13             25             4              8
#  5 2020-10-01    16             25             4              8
#  6 2020-11-01    17             25             4              8
#  7 2020-12-01    18             25             4              8
#  8 2021-01-01    17             25             4              8
#  9 2021-02-01    18             25             4              8
# 10 2021-03-01    16             25             4              8
# 11 2021-04-01    17             25             4              8
# 12 2021-05-01    13             25             4              8

df_manual_solution <- tibble(
  date = ymd("2020-06-01") + months(0:11),
  price = c(12, 11, 12, 13, 16, 17, 18, 17, 18, 16, 17, 13),
  total_capacity = 25,
  max_units_buy = 4,
  max_units_sell = 8,
  real_buy = c(4, 4, 4, 4, 4, 4, 0, 0, 0, 4, 0, 0),
  real_sell = c(0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 4, 0),
  inventory_level = cumsum(real_buy) - cumsum(real_sell),
  profit_loss = cumsum(real_sell*price) - cumsum(real_buy*price))

# date             price          total_capacity max_units_buy  max_units_sell real_buy real_sell inventory_level profit_loss
#  1 2020-06-01    12             25             4              8        4         0               4         -48
#  2 2020-07-01    11             25             4              8        4         0               8         -92
#  3 2020-08-01    12             25             4              8        4         0              12        -140
#  4 2020-09-01    13             25             4              8        4         0              16        -192
#  5 2020-10-01    16             25             4              8        4         0              20        -256
#  6 2020-11-01    17             25             4              8        4         0              24        -324
#  7 2020-12-01    18             25             4              8        0         8              16        -180
#  8 2021-01-01    17             25             4              8        0         8               8         -44
#  9 2021-02-01    18             25             4              8        0         8               0         100
# 10 2021-03-01    16             25             4              8        4         0               4          36
# 11 2021-04-01    17             25             4              8        0         4               0         104
# 12 2021-05-01    13             25             4              8        0         0               0         104

person Jakub.Novotny    schedule 18.05.2020    source источник


Ответы (2)


Я считаю, что это можно смоделировать как небольшую модель смешанного целочисленного программирования (MIP).

введите здесь описание изображения

Вот реализация с использованием CVXR:

> library(CVXR)
> 
> # data
> price = c(12, 11, 12, 13, 16, 17, 18, 17, 18, 16, 17, 13)
> capacity = 25
> max_units_buy = 4
> max_units_sell = 8
> 
> # number of time periods
> NT <- length(price)
> 
> # Decision variables
> inv = Variable(NT,integer=T)
> buy = Variable(NT,integer=T)
> sell = Variable(NT,integer=T)
> 
> # Lag operator
> L = cbind(rbind(0,diag(NT-1)),0)
> 
> # optimization model
> problem <- Problem(Maximize(sum(price*(sell-buy))),
+                    list(inv == L %*% inv + buy - sell,
+                         inv >= 0, inv <= capacity,
+                         buy >= 0, buy <= max_units_buy,
+                         sell >= 0, sell <= max_units_sell))
> result <- solve(problem,verbose=T)
GLPK Simplex Optimizer, v4.47
84 rows, 36 columns, 119 non-zeros
*     0: obj =  0.000000000e+000  infeas = 0.000e+000 (12)
*    35: obj = -1.040000000e+002  infeas = 0.000e+000 (0)
OPTIMAL SOLUTION FOUND
GLPK Integer Optimizer, v4.47
84 rows, 36 columns, 119 non-zeros
36 integer variables, none of which are binary
Integer optimization begins...
+    35: mip =     not found yet >=              -inf        (1; 0)
+    35: >>>>> -1.040000000e+002 >= -1.040000000e+002   0.0% (1; 0)
+    35: mip = -1.040000000e+002 >=     tree is empty   0.0% (0; 1)
INTEGER OPTIMAL SOLUTION FOUND
> cat("status:",result$status)
status: optimal
> cat("objective:",result$value)
objective: 104
> print(result$getValue(buy))
      [,1]
 [1,]    4
 [2,]    4
 [3,]    4
 [4,]    4
 [5,]    4
 [6,]    0
 [7,]    0
 [8,]    4
 [9,]    0
[10,]    4
[11,]    0
[12,]    0
> print(result$getValue(sell))
      [,1]
 [1,]    0
 [2,]    0
 [3,]    0
 [4,]    0
 [5,]    0
 [6,]    8
 [7,]    8
 [8,]    0
 [9,]    8
[10,]    0
[11,]    4
[12,]    0
> print(result$getValue(inv))
      [,1]
 [1,]    4
 [2,]    8
 [3,]   12
 [4,]   16
 [5,]   20
 [6,]   12
 [7,]    4
 [8,]    8
 [9,]    0
[10,]    4
[11,]    0
[12,]    0
> 
person Erwin Kalvelagen    schedule 19.05.2020
comment
Это выглядит очень многообещающе. Как вы думаете, можно ли добавить более сложные ограничения? Примером может служить то, что если уровень запасов превышает 50% от его общей емкости, вы можете продать 8 единиц (как в исходном вопросе). Однако, если он ниже 50%, вы можете продать только 6? - person Jakub.Novotny; 19.05.2020
comment
да. Это возможно. Однако здесь есть проблема моделирования. Обычно мы рассматриваем buy и sell как события, происходящие в течение периода t (мы называем их переменными потока). inv является переменной запаса и измеряется в конце периода t. Сказать, что sell[t] и buy[t] зависят от inv[t], немного странно (мы идем назад во времени). Конечно, мы можем его смоделировать и решить (мы решаем как одновременные уравнения, поэтому мы можем делать это). Но это может не иметь смысла в реальном мире. Вероятно, нам следует посмотреть на inv[t-1], чтобы изменить buy[t] и sell[t]. - person Erwin Kalvelagen; 19.05.2020
comment
Согласились, что inv [t-1] больше подходит для покупки [t] и продажи [t]. Вы случайно не знаете, как я могу включить такое условие в код? Например. если inv [t-1] / capacity ›= 0,5, то продать [t] = max_units_sell, иначе продать [t] = 0,75 * max_units_sell - person Jakub.Novotny; 19.05.2020
comment
Вам нужна двоичная переменная, чтобы создать 2 сегмента для инвентаря. Математика несложная, но больше, чем я могу поместить в поле для комментариев. - person Erwin Kalvelagen; 19.05.2020
comment
Большое спасибо за ответ. Я задал новый вопрос с максимальной покупкой / продажей в зависимости от уровня запасов здесь stackoverflow.com/questions/61900256/. Не стесняйтесь присоединиться к нам :-) - person Jakub.Novotny; 19.05.2020

Добавлена ​​возможность иметь начальный инвентарь и создана функция для поэтапной оптимизации с учетом покупки / продажи, зависящей от уровня запасов.

library(tidyverse)
library(lubridate)
library(CVXR)

init_fce <- function(.df_storage, .df_bounds, .type = "max"){
  if(.type == "max"){
    .df_storage$max_buy <- max(.df_bounds$max_buy)
    .df_storage$max_sell <- max(.df_bounds$max_sell)
  } else if(.type == "min"){
    .df_storage$max_buy <- min(.df_bounds$max_buy)
    .df_storage$max_sell <- min(.df_bounds$max_sell)
  } else if(.type == "mean"){
    .df_storage$max_buy <- mean(.df_bounds$max_buy)
    .df_storage$max_sell <- mean(.df_bounds$max_sell)
  }

  .df_storage
}
optim_fce <- function(.df){
  
  # Decision variables
  m_inv_tot = Variable(nrow(.df), integer = T)
  m_buy = Variable(nrow(.df), integer = T)
  m_sell = Variable(nrow(.df), integer = T)
  # Lag operator
  m_L = cbind(rbind(0, diag(nrow(.df) - 1)), 0)
  
  objetive <- Maximize(sum(.df$price*(m_sell-m_buy)))
  
  constraints <- list(
    m_inv_tot == m_L %*% m_inv_tot + .df$inv_init + m_buy - m_sell, # L %*% result$getValue(inv) + result$getValue(buy) - result$getValue(sell)
    m_inv_tot >= 0, m_inv_tot <= .df$capacity,
    m_buy >= 0, m_buy <= .df$max_buy,
    m_sell >= 0, m_sell <= .df$max_sell
  )
  
  problem <- Problem(objetive, constraints)
  result <- solve(problem) # , verbose=T
  
  .df <- .df %>%
    mutate(
      buy = (result$getValue(m_buy) %>% as.vector()),
      sell = (result$getValue(m_sell)  %>% as.vector()),
      inventory_real = (result$getValue(m_inv_tot) %>% as.vector())
    )
  
  .df
}
set_limits_fce <- function(.df_storage, .df_bounds){
  .df_storage <- .df_storage %>%
    select(-max_buy, -max_sell) %>%
    mutate(capacity_usage_pct_prec = lag(inventory_real, default = inv_init[1])/capacity) %>%
    crossing(.df_bounds %>% select(-segment)) %>%
    filter(capacity_usage_pct_prec >= lbound, capacity_usage_pct_prec < ubound) %>%
    mutate(
      within_bounds = (buy <= max_buy) & (sell <= max_sell) 
    ) %>%
    select(-lbound, -ubound)

  .df_storage
}
get_results <- function(.df_storage){
  if( any(!.df_storage$within_bounds) ){
    print("result not within bounds")
  } else{
    .df_storage$profit <- .df_storage$sell * .df_storage$price - .df_storage$buy * .df_storage$price
    print(sum(.df_storage$profit))
  }
  
  .df_storage
}


A1_storage <- tibble(
  date = ymd("2020-06-01") + months(0:11),
  price = c(12, 11, 12, 13, 16, 17, 18, 17, 18, 16, 17, 13),
  inv_init = c(3, rep(0, 11)),
  capacity = 25
)

A2_bounds <- tibble(
  segment = c("0%-30%", "30%-65%", "65%-70%", "70%-100%"),
  lbound = c(0, 0.3, 0.65, 0.7), 
  ubound = c(0.3, 0.65, 0.7, 1),
  max_buy = c(4,3,2,2),
  max_sell = c(4,6,6,8)
)

B1_max <- init_fce(A1_storage, A2_bounds, .type = "max") %>%
  optim_fce() %>%
  set_limits_fce(.df_bounds = A2_bounds) %>%
  get_results() %>%
  optim_fce() %>%
  set_limits_fce(.df_bounds = A2_bounds) %>%
  get_results() %>%
  optim_fce() %>%
  set_limits_fce(.df_bounds = A2_bounds) %>%
  get_results() %>%
  optim_fce() %>%
  set_limits_fce(.df_bounds = A2_bounds) %>%
  get_results()
person Jakub.Novotny    schedule 07.11.2020