1
0

finance.lisp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.finance
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.finance)
  5. (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
  6. (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
  7. (defvar *default-symbols* '("USDRUB=X" "EURRUB=X" "GBPRUB=X"))
  8. (defparameter +chart-ranges+ (list (cons "day" (* 24 60))
  9. (cons "week" (* 7 24 60))
  10. (cons "month" (* 30 24 60))
  11. (cons "quarter" (* 91 24 60))
  12. (cons "year" (* 365 24 60))))
  13. (defun get-rates (&optional (symbols *default-symbols*))
  14. (let ((response (agets (json-request
  15. +yahoo-url+
  16. :parameters `(("symbols" . ,(format nil "~{~A~^,~}" symbols))))
  17. "quoteResponse")))
  18. (when (aget "error" response)
  19. (error "Error in rates request"))
  20. (loop for rate in (agets response "result")
  21. collect (cons (aget "symbol" rate)
  22. (aget "regularMarketPrice" rate)))))
  23. (defun get-brent ()
  24. (handler-case
  25. (let ((last (read-from-string
  26. (aget "last" (first (aget "quotes" (json-request +brent-url+)))))))
  27. (when (numberp last)
  28. last))
  29. (error (e) (log:error e))))
  30. (defparameter +btc-e-usd-url+ "https://btc-e.com/api/2/btc_usd/ticker" "BTC-e BTC/USD ticker url")
  31. (defun get-btc-e ()
  32. (handler-case
  33. (aget "last" (aget "ticker" (json-request +btc-e-usd-url+)))
  34. (error (e) (log:error e))))
  35. (defparameter +coindesk-btc-usd-url+ "https://api.coindesk.com/site/headerdata.json?currency=BTC" "coindesk.com api endpoint")
  36. (defun get-coindesk-btc-usd ()
  37. (handler-case
  38. (agets (json-request +coindesk-btc-usd-url+)
  39. "bpi" "USD" "rate_float")
  40. (error (e) (log:error "~A" e))))
  41. ;; Formatting
  42. (defvar *symbol-aliases*
  43. (alexandria:plist-hash-table
  44. '("BTCUSD" ("btc" "bitcoin" "биток")
  45. "BRENT" ("чёрная" "нефть" "brent" "oil")
  46. "USDRUB=X" ("зелёный" "бакс" "доллар" "usd")
  47. "EURRUB=X" ("гейро" "eur" "евро")
  48. "GBPRUB=X" ("британец" "gbp" "фунт" "стерлинг")
  49. "CZKRUB=X" ("чешка" "czk")
  50. "DKKRUB=X" ("дашка" "dkk")
  51. "ETHUSD=X" ("eth" "etherium" "эфир" "эфирум" "виталик"))
  52. :test #'equal))
  53. (defun get-symbol-label (symbol)
  54. (let ((aliases (gethash symbol *symbol-aliases*)))
  55. (if aliases
  56. (car aliases)
  57. (let ((lower (string-downcase symbol)))
  58. (when (equal "=x" (subseq lower (- (length lower) 2)))
  59. (setf lower (subseq lower 0 (- (length lower) 2))))
  60. (if (= 6 (length lower))
  61. (format nil "~A/~A" (subseq lower 0 3) (subseq lower 3))
  62. lower)))))
  63. ;; Charts
  64. (defun get-serie (series symbol)
  65. (mapcar #'(lambda (r) (list (car r) (caddr r)))
  66. (remove symbol series :key #'cadr :test-not #'equal)))
  67. (defun range (seq &key (key 'identity))
  68. (loop
  69. for item in seq
  70. for value = (funcall key item)
  71. minimizing value into min
  72. maximizing value into max
  73. finally (return (values min max))))
  74. (defvar *chart-points* 800 "Data points in chart")
  75. (defun make-chart (series symbols)
  76. (let* ((r (multiple-value-list (range series :key #'car)))
  77. (fmt (if (<= (- (second r) (first r))
  78. (* 60 60 36))
  79. '((:hour 2) ":" (:min 2))
  80. '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
  81. (adw-charting:with-line-chart ((+ 90 *chart-points*)
  82. (floor (* 3 *chart-points*) 4))
  83. (loop for symbol in symbols
  84. do (adw-charting:add-series (get-symbol-label symbol)
  85. (get-serie series symbol)))
  86. (adw-charting:set-axis
  87. :x "Time" :draw-gridlines-p t
  88. :label-formatter #'(lambda (v)
  89. (local-time:format-timestring
  90. nil
  91. (local-time:unix-to-timestamp v)
  92. :format fmt)))
  93. (adw-charting:set-axis :y "RUB" :draw-gridlines-p t :label-formatter "~,2F")
  94. (adw-charting:save-file "chart.png"))))
  95. ;; Database
  96. (def-db-init
  97. (db-execute "create table if not exists finance_ticker (ts, symbol, price, primary key (ts, symbol)) without rowid")
  98. (db-execute "create table if not exists finance_chat_symbols (chat_id, symbol, primary key (chat_id, symbol)) without rowid")
  99. (db-execute "create table if not exists finance_orders (chat_id, user_id, user_name, symbol, amount, open, open_at, close, close_at)"))
  100. (defun migrate ()
  101. (db-execute "delete from finance_ticker")
  102. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, usd from finance where usd is not null" "USDRUB=X")
  103. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, eur from finance where eur is not null" "EURRUB=X")
  104. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, gbp from finance where gbp is not null" "GBPRUB=X")
  105. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, brent from finance where brent is not null" "BRENT")
  106. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, btc from finance where btc is not null" "BTCUSD"))
  107. (defun db/add-finance (ts symbol price)
  108. (db-execute "insert into finance_ticker (ts, symbol, price) values (?, ?, ?)" ts symbol price))
  109. (defun db/get-last-finance (&optional (symbols *default-symbols*))
  110. (loop for symbol in symbols
  111. for last = (db-select "select ts, symbol, price from finance_ticker where symbol = ? order by ts desc limit 1"
  112. symbol)
  113. when last append last))
  114. (defun db/get-finance-at (symbol ts)
  115. (db-single "select price from finance_ticker where symbol = ? and ts <= ? order by ts desc limit 1"
  116. symbol ts))
  117. (defun db/get-chat-symbols (&optional chat-id)
  118. (mapcar #'car
  119. (if chat-id
  120. (db-select "select symbol from finance_chat_symbols where chat_id = ?" chat-id)
  121. (db-select "select distinct symbol from finance_chat_symbols union select symbol from finance_orders"))))
  122. (defun db/set-chat-symbols (chat-id symbols)
  123. (db-transaction
  124. (db-execute "delete from finance_chat_symbols where chat_id = ?" chat-id)
  125. (dolist (symbol symbols)
  126. (db-execute "insert into finance_chat_symbols (chat_id, symbol) values (?, ?)"
  127. chat-id symbol))))
  128. (defun db/get-series (after-ts &optional (symbols *default-symbols*) (avg 60))
  129. (when symbols
  130. (let ((sql (format nil
  131. "select ts/~a*~:*~a, symbol, avg(price) from finance_ticker where ts >= ? and symbol in (~{?~*~^, ~}) group by ts/~a, symbol order by ts"
  132. avg symbols avg)))
  133. (apply #'db-select sql (local-time:timestamp-to-unix after-ts) symbols))))
  134. (defun db/order-buy (chat-id user-id user-name symbol amount open open-at)
  135. (db-execute "insert into finance_orders (chat_id, user_id, user_name, symbol, amount, open, open_at) values (?, ?, ?, ?, ?, ?, ?)"
  136. chat-id user-id user-name symbol amount open open-at))
  137. (defun db/order-sell (row-id amount close close-at)
  138. (db-execute "update finance_orders set amount = ?, close = ?, close_at = ? WHERE rowid = ?"
  139. amount close close-at row-id))
  140. (defun db/orders-get (chat-id)
  141. (db-select "select rowid, user_id, user_name, symbol, amount, open, open_at, close, close_at from finance_orders where chat_id = ? order by open_at, close_at" chat-id))
  142. ;; Cron
  143. (defcron process-rates ()
  144. (let* ((ts (local-time:timestamp-to-unix (local-time:now)))
  145. (symbols (db/get-chat-symbols))
  146. (rates (when symbols (get-rates symbols)))
  147. (brent (get-brent))
  148. (btc (get-coindesk-btc-usd)))
  149. (loop for (symbol . price) in rates
  150. do (db/add-finance ts symbol price))
  151. (when brent
  152. (db/add-finance ts "BRENT" brent))
  153. (when btc
  154. (db/add-finance ts "BTCUSD" btc))))
  155. ;;; Hooks
  156. (defun get-label-symbol (label)
  157. (or
  158. (loop for symbol being the hash-keys in *symbol-aliases* using (hash-value aliases)
  159. when (member label aliases :test #'string-equal)
  160. do (return symbol))
  161. (string-upcase label)))
  162. (def-message-cmd-handler handler-rates (:rates)
  163. (let* ((symbols (or (mapcar #'get-label-symbol args)
  164. (db/get-chat-symbols chat-id)
  165. *default-symbols*))
  166. (last (db/get-last-finance symbols)))
  167. (bot-send-message chat-id
  168. (if last
  169. (format nil "~{~A *~$*~^, ~} @ _~A_"
  170. (loop for (ts symbol price) in last
  171. append (list (get-symbol-label symbol) price))
  172. (format-ts (local-time:unix-to-timestamp (caar last))))
  173. "Пока нет данных. Если тикер реальный, задай через /setrates <tickers>")
  174. :parse-mode "markdown")))
  175. (defun get-order-info (order)
  176. (destructuring-bind (row-id user-id user-name symbol amount-text open-text open-at close-text close-at)
  177. order
  178. (let* ((amount (parse-float amount-text))
  179. (open (parse-float open-text))
  180. (open-amount (* amount open))
  181. (close-at (or close-at (local-time:timestamp-to-unix (local-time:now))))
  182. (close (if close-text
  183. (parse-float close-text)
  184. (db/get-finance-at symbol close-at)))
  185. (close-amount (when close (* amount close)))
  186. (duration (- close-at open-at))
  187. (income (when close-amount (- close-amount open-amount)))
  188. (roi (when income (/ income open-amount)))
  189. (annual (when roi (* (/ roi duration) (* +day+ 365.25)))))
  190. `((:row-id . ,row-id)
  191. (:user-id . ,user-id)
  192. (:user-name . ,user-name)
  193. (:symbol . ,symbol)
  194. (:amount . ,amount)
  195. (:open . ,open)
  196. (:open-at . ,open-at)
  197. (:open-amount . ,open-amount)
  198. (:open-p . ,(null close-text))
  199. (:close . ,close)
  200. (:close-at . ,close-at)
  201. (:close-amount . ,close-amount)
  202. (:duration . ,duration)
  203. (:income . ,income)
  204. (:roi . ,roi)
  205. (:annual . ,annual)))))
  206. (defun print-order-info (info)
  207. (format nil "~A ~A ~A ~A @ ~$ - ~A @ ~$, заработал ~$, ROI ~$%"
  208. (agets info :user-name)
  209. (agets info :amount)
  210. (get-symbol-label (agets info :symbol))
  211. (format-ts (local-time:unix-to-timestamp (agets info :open-at)))
  212. (agets info :open)
  213. (if (agets info :open-p) "now" (format-ts (local-time:unix-to-timestamp (agets info :close-at))))
  214. (agets info :close)
  215. (agets info :income)
  216. (* 100 (agets info :roi))))
  217. (def-message-cmd-handler handle-hodl (:hodl :hodlers)
  218. (let ((orders (db/orders-get chat-id)))
  219. (bot-send-message chat-id
  220. (if orders (format nil "~{~A~^~%~}"
  221. (loop for order in orders
  222. collect (print-order-info (get-order-info order))))
  223. "Нет ходлеров :("))))
  224. (def-message-cmd-handler handler-charts (:charts)
  225. (telegram-send-chat-action chat-id "upload_photo")
  226. (let* ((args (mapcar 'string-downcase args))
  227. (all-symbols (db/get-chat-symbols))
  228. (symbols (or (intersection (mapcar #'get-label-symbol args) all-symbols :test 'equal) all-symbols))
  229. (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
  230. (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
  231. (avg (* 60 (cond
  232. (day-range (round day-range *chart-points*))
  233. (number)
  234. (:otherwise 1))))
  235. (after-ts (local-time:timestamp- (local-time:now)
  236. (* avg *chart-points*) :sec))
  237. (rates (db/get-last-finance symbols))
  238. (chart (make-chart (db/get-series after-ts symbols avg) symbols)))
  239. (telegram-send-photo chat-id chart
  240. :caption
  241. (format nil "~{~A ~$~^, ~} @ ~A"
  242. (loop for (ts symbol price) in rates
  243. append (list (get-symbol-label symbol) price))
  244. (format-ts (local-time:unix-to-timestamp (caar rates)))))))
  245. (def-message-cmd-handler handler-set-rates (:setrates)
  246. (when args
  247. (db/set-chat-symbols chat-id (mapcar #'get-label-symbol args)))
  248. (bot-send-message chat-id
  249. (format nil "Пишем ~{_~a_~#[~; и ~:;, ~]~}"
  250. (mapcar #'get-symbol-label (db/get-chat-symbols chat-id)))
  251. :parse-mode "markdown"))