Комплекс обработки данных в Clojure


Я работаю над личным проектом по анализу рынка. У меня есть структура данных, представляющая все последние поворотные моменты на рынке, которая выглядит следующим образом:

[{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}
 {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
 {:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}
 {:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}
 {:high 1.12215, :time "2016-08-02T23:00:00.000000Z"}
 {:high 1.12273, :time "2016-08-02T21:15:00.000000Z"}
 {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
 {:low 1.119215, :time "2016-08-02T12:30:00.000000Z"}
 {:low 1.118755, :time "2016-08-02T12:00:00.000000Z"}
 {:low 1.117575, :time "2016-08-02T06:00:00.000000Z"}
 {:low 1.117135, :time "2016-08-02T04:30:00.000000Z"}
 {:low 1.11624, :time "2016-08-02T02:00:00.000000Z"}
 {:low 1.115895, :time "2016-08-01T21:30:00.000000Z"}
 {:low 1.11552, :time "2016-08-01T11:45:00.000000Z"}
 {:low 1.11049, :time "2016-07-29T12:15:00.000000Z"}
 {:low 1.108825, :time "2016-07-29T08:30:00.000000Z"}
 {:low 1.10839, :time "2016-07-29T08:00:00.000000Z"}
 {:low 1.10744, :time "2016-07-29T05:45:00.000000Z"}
 {:low 1.10716, :time "2016-07-28T19:30:00.000000Z"}
 {:low 1.10705, :time "2016-07-28T18:45:00.000000Z"}
 {:low 1.106875, :time "2016-07-28T18:00:00.000000Z"}
 {:low 1.10641, :time "2016-07-28T05:45:00.000000Z"}
 {:low 1.10591, :time "2016-07-28T01:45:00.000000Z"}
 {:low 1.10579, :time "2016-07-27T23:15:00.000000Z"}
 {:low 1.105275, :time "2016-07-27T22:00:00.000000Z"}
 {:low 1.096135, :time "2016-07-27T18:00:00.000000Z"}]

Концептуально я хочу соответствовать :high/:low пары, определите диапазон цен (high-low) и среднюю точку (average of high & low), но я не хочу, чтобы создавались все возможные пары.

То, что я хочу сделать, это начать с 1-го пункта в коллекции {:high 1.121455, :time "2016-08-03T05:15:00.000000Z"} и пройти "вниз" через оставшуюся часть коллекции. коллекция, создавая пару с каждым элементом :low, пока я не нажму на следующий элемент :high. Как только я попаду в следующий пункт :high, меня больше не интересуют никакие другие пары. В этом случае создается только одна пара, которая является :high и 1 - й :low - я останавливаюсь на этом, потому что следующий (3-й) элемент является :high. Сгенерированная запись 1 должна выглядеть как {:price-range 0.000365, :midpoint 1.121272, :extremes [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}]}

Затем я перейду ко 2-му элементу в коллекции {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"} и пройдусь "вниз" по остальной части коллекции, создавая пару с каждый пункт :high, пока я не нажму на следующий пункт :low. В этом случае я получаю 5 новых записей, генерируемых, будучи :low и следующие 5 :high элементов, которые являются последовательными; первая из этих 5 записей будет выглядеть как

{:price-range 0.000064, :midpoint 1.12131, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}]}

Вторая из этих 5 записей будет выглядеть так:

{:price-range 0.000835, :midpoint 1.1215075, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}]}

И так далее.

После этого я получаю :low, поэтому я останавливаюсь на этом.

Затем я переходил к 3-му пункту {:high 1.12173, :time "2016-08-03T04:30:00.000000Z"} и шел "вниз", создавая пары с каждым :low, пока не попадал в следующий :high. В в этом случае я получаю 0 пар, генерируемых, потому что за :high немедленно следует другой :high. То же самое для следующих 3: высоких пунктов, за которыми сразу же следует другой :high

Далее я добираюсь до 7-го пункта {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}, и это должно генерировать пару с каждым из следующих 20 пунктов :low.

Моим сгенерированным результатом будет список всех созданных пар:

[{:price-range 0.000365, :midpoint 1.121272, :extremes [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}]}
 {:price-range 0.000064, :midpoint 1.12131, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}]}
 ...

Если бы я реализовывал это с помощью чего-то вроде Python, я бы, вероятно, использовал пару вложенных циклов, используйте a break, чтобы выйти из внутреннего цикла, когда я перестал видеть :high s для сопряжения с моим :low и наоборот, и накапливать все сгенерированные записи в массив по мере прохождения 2 циклов. Я просто не могу придумать хороший способ атаковать его с помощью Clojure...

Есть идеи?

1 4

1 ответ:

Прежде всего, вы можете перефразировать это следующим образом:

  1. вы должны найти все граничные точки, где :high следует за :low, или наоборот
  2. Вам нужно взять элемент до привязки и сделать что-то с ним и каждым элементом после привязки, но до следующего переключения привязки.

Для простоты воспользуемся следующей моделью данных:

(def data0 [{:a 1} {:b 2} {:b 3} {:b 4} {:a 5} {:a 6} {:a 7}])

Первая часть может быть достигнута с помощью функции partition-by, которая разбивает сбор входных данных каждый раз, когда функция изменяет свое значение для обрабатываемого элемента:

user> (def step1 (partition-by (comp boolean :a) data0))
#'user/step1
user> step1
(({:a 1}) ({:b 2} {:b 3} {:b 4}) ({:a 5} {:a 6} {:a 7}))
Теперь вам нужно взять каждую из этих двух групп и манипулировать ими. группы должны быть такими: [({: a 1}) ({: b 2} {:b 3} {: b 4})] [({: b 2} {:b 3} {:b 4}) ({: a 5} {:A 6} {: a 7})]

Это достигается функцией partition:

user> (def step2 (partition 2 1 step1))
#'user/step2
user> step2
((({:a 1}) ({:b 2} {:b 3} {:b 4})) 
 (({:b 2} {:b 3} {:b 4}) ({:a 5} {:a 6} {:a 7})))
Вы должны что-то сделать для каждой пары групп. Вы можете сделать это с помощью map:
user> (def step3 (map (fn [[lbounds rbounds]]
                    (map #(vector (last lbounds) %)
                         rbounds))
                  step2))
#'user/step3
user> step3
(([{:a 1} {:b 2}] [{:a 1} {:b 3}] [{:a 1} {:b 4}]) 
 ([{:b 4} {:a 5}] [{:b 4} {:a 6}] [{:b 4} {:a 7}]))

Но так как вы нуждаетесь в объединенный список, а не сгруппированный, вы хотели бы использовать mapcat вместо map:

user> (def step3 (mapcat (fn [[lbounds rbounds]]
                           (map #(vector (last lbounds) %)
                                rbounds))
                         step2))
#'user/step3
user> step3
([{:a 1} {:b 2}] 
 [{:a 1} {:b 3}] 
 [{:a 1} {:b 4}] 
 [{:b 4} {:a 5}] 
 [{:b 4} {:a 6}] 
 [{:b 4} {:a 7}])

Вот результат, которого мы хотим (это почти так, поскольку мы просто генерируем векторы, а не карты).

Теперь вы можете украсить его с помощью макроса threading:

(->> data0
     (partition-by (comp boolean :a))
     (partition 2 1)
     (mapcat (fn [[lbounds rbounds]]
               (map #(vector (last lbounds) %)
                    rbounds))))

, который дает вам точно такой же результат.

Применительно к вашим данным это будет выглядеть почти так же (с другим результатом, генерирующим fn)

user> (defn hi-or-lo [item]
        (item :high (item :low)))
#'user/hi-or-lo
user> 
(->> data
     (partition-by (comp boolean :high))
     (partition 2 1)
     (mapcat (fn [[lbounds rbounds]]
               (let [left-bound (last lbounds)
                     left-val (hi-or-lo left-bound)]
                 (map #(let [right-val (hi-or-lo %)
                             diff (Math/abs (- right-val left-val))]
                         {:extremes [left-bound %]
                          :price-range diff
                          :midpoint (+ (min right-val left-val)
                                       (/ diff 2))})
                      rbounds))))
     (clojure.pprint/pprint))

Он печатает следующее:

({:extremes
  [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}
   {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}],
  :price-range 3.6500000000017074E-4,
  :midpoint 1.1212725}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}],
  :price-range 6.399999999999739E-4,
  :midpoint 1.12141}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}],
  :price-range 8.350000000001412E-4,
  :midpoint 1.1215074999999999}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12215, :time "2016-08-02T23:00:00.000000Z"}],
  :price-range 0.001060000000000061,
  :midpoint 1.12162}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12273, :time "2016-08-02T21:15:00.000000Z"}],
  :price-range 0.0016400000000000858,
  :midpoint 1.12191}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}],
  :price-range 0.0022900000000001253,
  :midpoint 1.1222349999999999}
 {:extremes
  [{:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
   {:low 1.119215, :time "2016-08-02T12:30:00.000000Z"}],
  :price-range 0.004164999999999974,
  :midpoint 1.1212975}
 {:extremes
  [{:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
   {:low 1.118755, :time "2016-08-02T12:00:00.000000Z"}],
  :price-range 0.004625000000000101,
  :midpoint 1.1210675}
 ...
В качестве ответа на вопрос о "сложных манипуляциях с данными" я бы посоветовал вам просмотреть все манипулирующие функции коллекций из ядра clojure, а затем попытаться разложить любую задачу на их применение. Не так уж много случаев, когда вам нужно что-то сверх них.