diff --git a/dev/src/dev/backtest/backtest.clj b/dev/src/dev/backtest/backtest.clj index 10eb53d..ecb73e2 100644 --- a/dev/src/dev/backtest/backtest.clj +++ b/dev/src/dev/backtest/backtest.clj @@ -53,12 +53,8 @@ bar-ds ; :exit-idx ])) - ; | :side | :qty | :entry-price | :exit-price | :entry-date | :exit-date | :reason | ; |--------+------+--------------+-------------------+----------------------+----------------------+--------------| ; | :long | 1.0 | 100 | 101.0 | 2024-09-02T00:00:00Z | 2024-09-03T00:00:00Z | :profit-prct | ; | :long | 1.0 | 100 | 101.0 | 2024-09-05T00:00:00Z | 2024-09-07T00:00:00Z | :profit-prct | ; | :short | 1.0 | 100 | 99.00990099009901 | 2024-09-08T00:00:00Z | 2024-09-09T00:00:00Z | :profit-prct | - - - diff --git a/dev/src/dev/backtest/rule.clj b/dev/src/dev/backtest/rule.clj index a29bcef..2f22d7f 100644 --- a/dev/src/dev/backtest/rule.clj +++ b/dev/src/dev/backtest/rule.clj @@ -1,22 +1,24 @@ (ns dev.backtest.rule (:require [tick.core :as t] - [quanta.trade.entry-signal.rule :as rule])) + [quanta.trade.entry-signal.core :as rule])) (def m (rule/create-entrysignal-manager {:asset "EUR/USD" :entry {:type :fixed-qty :fixed-qty 1.0} :exit [{:type :profit-prct :prct 1.0} {:type :profit-prct :prct 5.0}]})) +;; => #'dev.backtest.rule/m + m -;; => {:positions #, +;; => {:positions #, ;; :asset "EUR/USD", -;; :entrysize-fn #function[quanta.trade.entry-signal.rule.entry/eval40973/fn--40975/fn--40977], -;; :exit-rules creating profit-prct exit rule opts: {:type :profit-prct, :prct 1.0} -;; creating profit-prct exit rule opts: {:type :profit-prct, :prct 5.0} -;; (#function[quanta.trade.entry-signal.rule.exit/eval49683/fn--49685/fn--49688] -;; #function[quanta.trade.entry-signal.rule.exit/eval49683/fn--49685/fn--49688])} +;; :entrysize-fn #function[quanta.trade.entry-signal.entry.core/eval8847/fn--8849/fn--8851], +;; :exit-rules +;; (#function[quanta.trade.entry-signal.exit.config/eval9125/fn--9127/fn--9130] +;; #function[quanta.trade.entry-signal.exit.config/eval9125/fn--9127/fn--9130])} + (rule/on-position-open m {:id 5 :asset "EUR/USD" @@ -26,6 +28,38 @@ m :entry-date (t/instant "2023-01-03T00:00:00Z") :qty 100000 }) +;; => {5 +;; {:position +;; {:id 5, +;; :asset "EUR/USD", +;; :side :long, +;; :entry-price 1.1, +;; :entry-idx 107, +;; :entry-date #time/instant "2023-01-03T00:00:00Z", +;; :qty 100000}, +;; :manager +;; {:rules +;; ({:position +;; {:id 5, +;; :asset "EUR/USD", +;; :side :long, +;; :entry-price 1.1, +;; :entry-idx 107, +;; :entry-date #time/instant "2023-01-03T00:00:00Z", +;; :qty 100000}, +;; :level 1.1110000000000002, +;; :label :profit-prct} +;; {:position +;; {:id 5, +;; :asset "EUR/USD", +;; :side :long, +;; :entry-price 1.1, +;; :entry-idx 107, +;; :entry-date #time/instant "2023-01-03T00:00:00Z", +;; :qty 100000}, +;; :level 1.1550000000000002, +;; :label :profit-prct})}}} + ;; => {5 ;; {:position ;; {:id 5, @@ -37,13 +71,13 @@ m ;; :qty 100000}, ;; :position-fn #function[quanta.trade.entry-signal.rule.exit/position-rules/fn--49854]}} -(rule/check-exit m {:ds nil - :row {:high 1.10 :low 1.09 :idx 1000 :date (t/instant)}}) +(rule/check-exit m {:high 1.10 :low 1.09 :idx 1000 :date (t/instant)}) +;; => () + ;; () -(rule/check-exit m {:ds nil - :row {:high 1.12 :low 1.09 :idx 1001 :date (t/instant)}}) -;; ({:entry-date #time/instant "2023-01-03T00:00:00Z", +(rule/check-exit m {:high 1.12 :low 1.09 :idx 1001 :date (t/instant)}) +;; => ({:entry-date #time/instant "2023-01-03T00:00:00Z", ;; :entry-price 1.1, ;; :reason :profit-prct, ;; :exit-idx 1001, @@ -52,41 +86,29 @@ m ;; :qty 100000, ;; :exit-price 1.1110000000000002, ;; :asset "EUR/USD", -;; :exit-date #time/instant "2024-10-07T04:14:31.986421257Z"}) +;; :exit-date #time/instant "2024-10-10T13:08:48.806873734Z"}) -(rule/check-exit m {:ds nil - :row {:high 1.20 :low 1.09 :idx 1002 :date (t/instant)}}) -;; ({:id 5, -;; :asset "EUR/USD", +(rule/check-exit m {:high 1.20 :low 1.09 :idx 1002 :date (t/instant)}) +;; => ({:entry-date #time/instant "2023-01-03T00:00:00Z", +;; :entry-price 1.1, ;; :reason :profit-prct, ;; :exit-idx 1002, +;; :id 5, +;; :side :long, +;; :qty 100000, ;; :exit-price 1.1110000000000002, -;; :exit-date #time/instant "2024-10-07T04:05:51.775349999Z"}) - -(rule/check-exit m {:ds nil - :row {:high 1.20 :low 1.09 :idx 1002 - :close 1.07 :date (t/instant) :entry :long}}) - -;; ({:id 5, ;; :asset "EUR/USD", +;; :exit-date #time/instant "2024-10-10T13:08:58.455516934Z"}) + +(rule/check-exit m {:high 1.20 :low 1.09 :idx 1002 + :close 1.07 :date (t/instant) :entry :long}) +;; => ({:entry-date #time/instant "2023-01-03T00:00:00Z", +;; :entry-price 1.1, ;; :reason :profit-prct, ;; :exit-idx 1002, +;; :id 5, +;; :side :long, +;; :qty 100000, ;; :exit-price 1.1110000000000002, -;; :exit-date #time/instant "2024-10-07T04:06:35.945787055Z"}) - - - -(def row {:close 100.0 :entry :long - :idx 107 :date }) - -(eventually-entry-position "QQQ" [:fixed-qty 3.1] row) -(eventually-entry-position "QQQ" [:fixed-amount 15000.0] row) - - - - -(def m-bad (esm/create-entrysignal-manager - {:entry nil - :exit [{:type :glorified-dummy :a 1.0}]})) - -m-bad +;; :asset "EUR/USD", +;; :exit-date #time/instant "2024-10-10T13:09:56.032519040Z"}) \ No newline at end of file diff --git a/dev/src/dev/exit/trailing_stop.clj b/dev/src/dev/exit/trailing_stop.clj index d498714..f1feeaa 100644 --- a/dev/src/dev/exit/trailing_stop.clj +++ b/dev/src/dev/exit/trailing_stop.clj @@ -1,7 +1,7 @@ (ns dev.exit.trailing-stop (:require - [quanta.trade.entry-signal.rule.exit2 :refer [check-exit]] - [quanta.trade.entry-signal.rule.exit-config :refer [exit-rule]])) + [quanta.trade.entry-signal.exit.exit2 :refer [check-exit]] + [quanta.trade.entry-signal.exit.exit-config :refer [exit-rule]])) (def configured-rule (exit-rule {:type :trailing-stop-offset :col :atr})) diff --git a/src/quanta/trade/backtest/from_entry.clj b/src/quanta/trade/backtest/from_entry.clj index 66d7d50..082798d 100644 --- a/src/quanta/trade/backtest/from_entry.clj +++ b/src/quanta/trade/backtest/from_entry.clj @@ -18,12 +18,8 @@ r (range n) ds-idx (tc/add-column ds :idx r)] (loop [idx 0] - (let [ds-until-idx (tc/select-rows ds-idx (range 0 (inc idx))) - row (row-at ds-idx idx) - output {;:idx idx - :data {:row row - :ds nil ; ds-until-idx - }} + (let [row (row-at ds-idx idx) + output {:data row} entry (:entry row) output (case entry :long (assoc output :entry-signal entry) diff --git a/src/quanta/trade/backtest2.clj b/src/quanta/trade/backtest2.clj index f092074..15d09e3 100644 --- a/src/quanta/trade/backtest2.clj +++ b/src/quanta/trade/backtest2.clj @@ -4,7 +4,7 @@ [tablecloth.api :as tc] [missionary.core :as m] [quanta.trade.commander :as cmd] - [quanta.trade.entry-signal.rule :as rule] + [quanta.trade.entry-signal.core :as rule] [quanta.trade.backtest.commander :refer [create-position-commander]] [quanta.trade.backtest.from-entry :refer [from-algo-ds]])) diff --git a/src/quanta/trade/entry_signal/core.clj b/src/quanta/trade/entry_signal/core.clj new file mode 100644 index 0000000..d7bca95 --- /dev/null +++ b/src/quanta/trade/entry_signal/core.clj @@ -0,0 +1,75 @@ +(ns quanta.trade.entry-signal.core + (:require + [quanta.trade.entry-signal.entry.core :as entry] + [quanta.trade.entry-signal.exit.config :refer [exit-rule]] + [quanta.trade.entry-signal.exit.position :as exit]) + (:import + [quanta.trade.entry_signal.exit.position MultipleRules])) + +(defn create-entrysignal-manager [{:keys [asset entry exit]}] + {:positions (atom {}) + :asset asset + :entrysize-fn (entry/positionsize2 entry) + :exit-rules (map exit-rule exit)}) + +;; entry + +(defn create-entry [this data] + (println "rule/create-entry: " data) + (entry/create-position this data)) + +; on position open/close + +(defn create-exit-manager-for-position + "create a exit-fn for one position. + this gets run on each bar, while the positon is open" + [rules position] + ;(println "creating exit-rules for position: " position) + ;(println "exit rules: " rules) + (let [position-rules (map #(% position) rules)] + (MultipleRules. position-rules))) + +(defn on-position-open + "on-position-open is an event that gets emitted by trade-commander. + we need to start new exit-rules for a new position here." + [{:keys [exit-rules positions]} position] + (assert exit-rules "rule manager state needs to have :exit-rules") + (println "rule/on-position-open: " position) + (swap! positions assoc + (:id position) + {:position position + :manager (create-exit-manager-for-position exit-rules position)})) + +(defn on-position-close [{:keys [positions]} position] + (println "rule/on-position-close: " position) + (swap! positions dissoc (:id position))) + +(defn check-exit-position [{:keys [position manager]} row] + ;(println "check-exit-position: " position) + ;; {:id 5, :asset EUR/USD, :side :long, + ;; :entry-price 1.1, :qty 100000} + (when-let [exit (exit/check-exit manager row)] ; [:profit-prct 1.1110000000000002] + (let [[reason exit-price] exit + {:keys [id asset side entry-price entry-date qty]} position + {:keys [idx date]} row] + {:id id + :asset asset + :side side + :qty qty + :entry-price entry-price + :entry-date entry-date + ; exit + :reason reason + :exit-idx idx + :exit-price exit-price + :exit-date date}))) + +(defn check-exit [{:keys [positions]} row] + (println "rule/check-exit: " row) + (->> (vals @positions) + (map #(check-exit-position % row)) + (remove nil?))) + + + + diff --git a/src/quanta/trade/entry_signal/rule/entry.clj b/src/quanta/trade/entry_signal/entry/core.clj similarity index 67% rename from src/quanta/trade/entry_signal/rule/entry.clj rename to src/quanta/trade/entry_signal/entry/core.clj index 5477840..e950221 100644 --- a/src/quanta/trade/entry_signal/rule/entry.clj +++ b/src/quanta/trade/entry_signal/entry/core.clj @@ -1,4 +1,4 @@ -(ns quanta.trade.entry-signal.rule.entry) +(ns quanta.trade.entry-signal.entry.core) (defmulti positionsize2 (fn [{:keys [type] :as opts}] type)) @@ -18,12 +18,11 @@ (and signal ; signal might be nil (contains? #{:long :short} signal))) -(defn create-position [{:keys [asset entrysize-fn]} - {:keys [row] :as data}] - (let [{:keys [date idx close entry] } row] - {:side entry +(defn create-position [{:keys [asset entrysize-fn]} + {:keys [date idx close entry] :as row}] + {:side entry :asset asset :qty (entrysize-fn close) :entry-idx idx :entry-date date - :entry-price close})) + :entry-price close}) diff --git a/src/quanta/trade/entry_signal/exit/config.clj b/src/quanta/trade/entry_signal/exit/config.clj new file mode 100644 index 0000000..e7e8750 --- /dev/null +++ b/src/quanta/trade/entry_signal/exit/config.clj @@ -0,0 +1,49 @@ +(ns quanta.trade.entry-signal.exit.config + (:require + [quanta.trade.entry-signal.exit.position :as e]) + (:import + [quanta.trade.entry_signal.exit.position TakeProfit TrailingStopLoss MultipleRules])) + +(defmulti exit-rule + (fn [{:keys [type] :as opts}] + type)) + +(defmethod exit-rule :profit-prct [{:keys [label prct] + :or {label :profit-prct}}] + (assert prct "take-profit-prct needs :prct parameter") + (fn [{:keys [entry-price side] :as position}] + ;(println "creating profit-prct rule for position: " position) + (assert entry-price "take-profit-prct needs :position :entry-price") + (assert side "take-profit-prct needs :position :side") + (let [prct (/ prct 100.0) + level (case side + :long (* entry-price (+ 1.0 prct)) + :short (/ entry-price (+ 1.0 prct)))] + (TakeProfit. position level label)))) + +(defmethod exit-rule :trailing-stop-offset [{:keys [col label] + :or {label :trailing-stop}}] + (assert col "trailing-stop-offset needs :col parameter") + (fn [position] + ;(assert entry-price "trailing-stop-offset needs :position :entry-price") + ;(assert side "trailing-stop-offset needs :position :side") + (let [;_ (assert offset (str "trailing-stop-offset needs :row " col " value")) + level-initial nil + level-a (atom level-initial) + new-level-fn (fn [position level row] + (let [{:keys [entry-price side]} position + offset (get row col) + close (:close row) + offset (get row col)] + (if level + (case side + :long (- close offset) + :short (+ close offset)) + (case side + :long (- entry-price offset) + :short (+ entry-price offset)))))] + (TrailingStopLoss. position level-a new-level-fn label)))) + + + + diff --git a/src/quanta/trade/entry_signal/rule/exit2.clj b/src/quanta/trade/entry_signal/exit/position.clj similarity index 85% rename from src/quanta/trade/entry_signal/rule/exit2.clj rename to src/quanta/trade/entry_signal/exit/position.clj index 08607d7..97c2f8f 100644 --- a/src/quanta/trade/entry_signal/rule/exit2.clj +++ b/src/quanta/trade/entry_signal/exit/position.clj @@ -1,4 +1,4 @@ -(ns quanta.trade.entry-signal.rule.exit2) +(ns quanta.trade.entry-signal.exit.position) (defprotocol IExit (priority [_]) @@ -90,10 +90,12 @@ new-level (new-level-fn position @level-a row) new-level (case (:side position) :short - (when (< new-level @level-a) + (when (or (nil? @level-a) + (< new-level @level-a)) new-level) :long - (when (> new-level @level-a) + (when (or (nil? @level-a) + (> new-level @level-a)) new-level))] (when new-level (println "TrailingStopLoss changes from " @level-a " to: " new-level) @@ -101,3 +103,11 @@ r))) +(defrecord MultipleRules [rules] + IExit + (check-exit [_ {:keys [high low] :as row}] + (->> rules + (map #(check-exit % row)) + (remove nil?) + first))) + diff --git a/src/quanta/trade/entry_signal/rule.clj b/src/quanta/trade/entry_signal/rule.clj deleted file mode 100644 index 49d8455..0000000 --- a/src/quanta/trade/entry_signal/rule.clj +++ /dev/null @@ -1,34 +0,0 @@ -(ns quanta.trade.entry-signal.rule - (:require - [quanta.trade.entry-signal.rule.entry :as entry] - [quanta.trade.entry-signal.rule.exit :as exit])) - -(defn create-entrysignal-manager [{:keys [asset entry exit]}] - {:positions (atom {}) - :asset asset - :entrysize-fn (entry/positionsize2 entry) - :exit-rules (map exit/exit-rule exit)}) - -(defn on-position-open - "on-position-open is an event that gets emitted by trade-commander. - we need to start new exit-rules for a new position here." - [{:keys [exit-rules positions]} position] - (assert exit-rules "rule manager state needs to have :exit-rules") - (println "rule/on-position-open: " position) - (let [position-fn (exit/position-rules exit-rules position)] - (swap! positions assoc (:id position) {:position position - :position-fn position-fn}))) - -(defn on-position-close [{:keys [positions]} position] - (println "rule/on-position-close: " position) - (swap! positions dissoc (:id position))) - -(defn check-exit [this data] - (println "rule/check-exit: " data) - (exit/check-exit-rules this data)) - -(defn create-entry [this data] - (println "rule/create-entry: " data) - (entry/create-position this data)) - - diff --git a/src/quanta/trade/entry_signal/rule/exit.clj b/src/quanta/trade/entry_signal/rule/exit.clj deleted file mode 100644 index cb02607..0000000 --- a/src/quanta/trade/entry_signal/rule/exit.clj +++ /dev/null @@ -1,112 +0,0 @@ -(ns quanta.trade.entry-signal.rule.exit) - -;; on-position-open: (exit opts position) -;; which returns: (fn [ds row]) -;; -;; an exit rule gets created when a position is opened. -;; -;; the opts parameter is a map, whose :type key is mandatory -;; and is used to construct different exti-rule-types. -;; stateful exit rules, can create a state atom when -;; a position is opened. -;; a :label parameter can be passed, this is used to identify -;; the exit reason, by default it is set to the :type, as -;; we assume in the simplest scenario there is only one -;; :type :profit-target for example -;; -;; (fn [ds row]) -;; for each bar the position is open this fn is called -;; ds is the dataset up to the current bar, row is -;; the last row of the dataset as a map. - -(defmulti exit-rule - "returns a closed roundtrip or nil. - input: position + row" - (fn [{:keys [type]}] type)) - - -(defn- take-profit [{:keys [target-price label] - :or {label :take-profit} - :as opts} - {:keys [side] :as position}] - (case side - :long - (fn [{:keys [row]}] - ;(println "take-profit-long check: " row) - (let [{:keys [high]} row] - (when (>= high target-price) - [label target-price]))) - :short - (fn [{:keys [row]}] - ;(println "take-profit-short check: " row) - (let [{:keys [low]} row] - (when (<= low target-price) - [label target-price]))))) - -(defmethod exit-rule :profit-prct [{:keys [label prct] - :or {label :profit-prct} - :as opts}] - ;(println "creating profit-prct exit rule opts: " opts) - (assert prct "take-profit-prct needs :prct parameter") - (fn [{:keys [entry-price side] :as position}] - ;(println "creating profit-prct rule for position: " position) - (assert entry-price "exit-rule needs :entry-price") - (assert side "exit-rule needs :side") - (let [prct (/ prct 100.0) - target-price (case side - :long (* entry-price (+ 1.0 prct)) - :short (/ entry-price (+ 1.0 prct)))] - ;(println "take-profit target: " target-price) - (take-profit (assoc opts - :label label - :target-price target-price) position)))) - -(defmethod exit-rule :default [{:keys [type] - :as opts}] - (throw (ex-info "unkown exit-rule type" opts))) - -(defn position-rules - "create a exit-fn for one position. - this gets run on each bar, while the positon is open" - [rules position] - ;(println "creating exit-rules for position: " position) - ;(println "exit rules: " rules) - (let [position-rules (map #(% position) rules)] - (fn [data] - (->> (map #(% data) position-rules) - (remove nil?) - first)))) - - -(defn check-exit-position [{:keys [position - position-fn] - :as p} data] - ;(println "check-exit-position: " position) - ;; {:id 5, :asset EUR/USD, :side :long, - ;; :entry-price 1.1, :qty 100000} - - (when-let [exit (position-fn data)] ; [:profit-prct 1.1110000000000002] - (let [[reason exit-price] exit - {:keys [id asset side entry-price entry-date qty]} position - {:keys [idx date]} (:row data) - ] - {:id id - :asset asset - :side side - :qty qty - :entry-price entry-price - :entry-date entry-date - ; exit - :reason reason - :exit-idx idx - :exit-price exit-price - :exit-date date - }))) - -(defn check-exit-rules [{:keys [positions]} data] - (->> (vals @positions) - (map #(check-exit-position % data)) - (remove nil?))) - - - diff --git a/src/quanta/trade/entry_signal/rule/exit_config.clj b/src/quanta/trade/entry_signal/rule/exit_config.clj deleted file mode 100644 index 721b16f..0000000 --- a/src/quanta/trade/entry_signal/rule/exit_config.clj +++ /dev/null @@ -1,44 +0,0 @@ -(ns quanta.trade.entry-signal.rule.exit-config -(:require - [quanta.trade.entry-signal.rule.exit2 :as e]) - (:import [quanta.trade.entry_signal.rule.exit2 TakeProfit TrailingStopLoss]) - ) - -(defmulti exit-rule - (fn [{:keys [type] :as opts}] - type)) - - -(defmethod exit-rule :profit-prct [{:keys [label prct] - :or {label :profit-prct}}] - (assert prct "take-profit-prct needs :prct parameter") - (fn [{:keys [entry-price side] :as position} row] - ;(println "creating profit-prct rule for position: " position) - (assert entry-price "take-profit-prct needs :position :entry-price") - (assert side "take-profit-prct needs :position :side") - (let [prct (/ prct 100.0) - level (case side - :long (* entry-price (+ 1.0 prct)) - :short (/ entry-price (+ 1.0 prct)))] - (TakeProfit. position level label)))) - -(defmethod exit-rule :trailing-stop-offset [{:keys [col label] - :or {label :trailing-stop}}] - (assert col "trailing-stop-offset needs :col parameter") - (fn [{:keys [entry-price side] :as position} - row] - (assert entry-price "trailing-stop-offset needs :position :entry-price") - (assert side "trailing-stop-offset needs :position :side") - (let [offset (get row col) - _ (assert offset (str "trailing-stop-offset needs :row " col " value")) - level-initial (case side - :long (- entry-price offset) - :short (+ entry-price offset)) - level-a (atom level-initial) - new-level-fn (fn [_position _level row] - (let [close (:close row) - offset (get row col)] - (case side - :long (- close offset) - :short (+ close offset))))] - (TrailingStopLoss. position level-a new-level-fn label)))) \ No newline at end of file