├── .gitignore
├── LICENSE
├── README.md
├── examples
├── market_as_module.py
├── market_as_standalone_client.py
└── market_as_standalone_server.py
├── extra
└── DWX_ZeroMQ_Server_v2.0.1_RC8.mq4
├── livetrader
├── __init__.py
├── exceptions.py
├── lib
│ └── dwx_zeromq_connector.py
├── market
│ ├── __init__.py
│ ├── base.py
│ ├── cache.py
│ ├── dwx.py
│ └── tdx.py
├── rpc.py
├── trade
│ ├── __init__.py
│ └── base.py
└── utils.py
├── requirements.txt
├── setup.cfg
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv
2 | .vscode
3 | build
4 | dist
5 | *.egg-info
6 | __pycache__
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## livetrader
2 |
3 | ### 介绍
4 | livetrader 是一个整合了行情和交易接口的工具链。现在已经有很多比较成熟的交易框架了,比方说 [vnpy](https://github.com/vnpy/vnpy)、[quantaxis](https://github.com/QUANTAXIS/QUANTAXIS)、[backtrader](https://github.com/mementum/backtrader)、[zipline](https://github.com/quantopian/zipline)等,但有一个问题是这些框架都没有很好地解决的,那就是:不同框架对实盘行情、实盘交易的支持都不是很完整。
5 |
6 | 比方说 vnpy 的[交易接口](https://www.vnpy.com/docs/cn/gateway.html#id7)主要集中在数字货币、期货上,对于外汇只有 MT5 的接口,对于美股也是只有富途证券。再比方说 quantaxis 也主要集中在 A股和期货之上([链接](https://doc.yutiansut.com/datafetch)),对于实盘交易接口部分因为了解的不多,所以不做过多评价,但就看文档而言,好似并没有找到实盘的支持。更别说国外的几个框架了 backtrader、zipline 的实盘基本上就只有 盈透(美股)、oanda(外汇) 这两 broker 的支持。
7 |
8 | 这样就会让我们遇到一个问题就是如果我们是一个单策略、多品种的交易模式的话,如果要覆盖所有的可程序化交易的 期货、美股、外汇、数字货币 就需要在不同的平台上维护多份代码,想要交易MT4的,就只能使用 MQL 语言编写策略,想要交易美股盈透证券的,就需要用 backtrader 来写逻辑,而在国内要交易期货的话, vnpy 和 quantaxis 又是两个不一样的选型。
9 |
10 | 所以,这个项目的目标是为了补充这些框架里不足的那一部分,**集中把行情接口、交易接口统一化**,然后再针对现在比较流行的几个交易框架(vnpy、quantaxis、backtrader) 提供相关的接口代码,这样无论之前是在哪个框架下编写的策略,就都可以获取全品种数据、交易全品种了(期货、美股、外汇、数字货币)
11 |
12 | ### 使用方式
13 |
14 | #### 安装
15 |
16 | ```bash
17 | pip install -U livetrader
18 | ```
19 | 使用:可以参考 examples 目录里的代码
20 |
21 | ```python
22 | # 作为模块启动
23 | import asyncio
24 |
25 | from simpletrader.market import (CachedMarket, DwxMarket, MarketService,
26 | TdxMarket)
27 |
28 |
29 | async def listern_to_latest_kline(kline_queue):
30 | print('get latest kline')
31 | for i in range(5):
32 | (symbol, kline) = await kline_queue.get()
33 | print("%s, %s" % (symbol, kline))
34 |
35 |
36 | async def get_kline_histories(service):
37 | print('get kline histories')
38 | histories = await service.get_kline_histories('US.BABA', limit=100)
39 | print(len(histories))
40 |
41 |
42 | async def main():
43 | market = TdxMarket(host='xx.xx.xx.xx:7727')
44 | service = MarketService(market, ['US.BABA'])
45 | kline_queue = service.start()
46 | await listern_to_latest_kline(kline_queue)
47 | await get_kline_histories(service)
48 | service.stop()
49 |
50 | if __name__ == "__main__":
51 | asyncio.run(main())
52 |
53 | ```
54 |
55 | ```python
56 | # 作为服务启动
57 | import logging
58 |
59 | from livetrader.market import CachedMarket, DwxMarket, MarketService, TdxMarket
60 | from livetrader.rpc import Server
61 |
62 |
63 | def create_server():
64 | market = TdxMarket(host='xx.xx.xx.xx:7727')
65 | service = MarketService(market, ['US.BABA'])
66 | server = Server(service)
67 | server.bind('ipc:///tmp/market')
68 | return server
69 |
70 |
71 | if __name__ == "__main__":
72 | logging.basicConfig(format=' %(name)s :: %(levelname)-8s :: %(message)s')
73 | root_logger = logging.getLogger()
74 | root_logger.setLevel(logging.DEBUG)
75 | server = create_server()
76 | server.run()
77 |
78 | ```
79 | ```python
80 | # 对应服务的客户端
81 | import asyncio
82 | import logging
83 |
84 | from livetrader.rpc import Client, MarketSubscriber
85 |
86 |
87 | class PrintSubscriber(MarketSubscriber):
88 |
89 | def on_kline(self, kline: dict):
90 | print('recv kline: %s' % kline, flush=True)
91 |
92 |
93 | def subscribe_kline(endpoint: str, symbol: str):
94 | subscriber = PrintSubscriber(symbol)
95 | subscriber.connect('ipc:///tmp/market')
96 | subscriber.run()
97 | return subscriber
98 |
99 |
100 | async def main():
101 | endpoint = "ipc:///tmp/market"
102 | symbol = 'US.BABA'
103 | subscriber = subscribe_kline(endpoint, symbol)
104 | await asyncio.sleep(5)
105 | subscriber.close()
106 |
107 | client = Client()
108 | client.connect(endpoint)
109 | histories = client.get_kline_histories(symbol, None, 100)
110 | print(len(histories))
111 | client.close()
112 |
113 |
114 | if __name__ == "__main__":
115 | logging.basicConfig(format=' %(name)s :: %(levelname)-8s :: %(message)s')
116 | root_logger = logging.getLogger()
117 | root_logger.setLevel(logging.DEBUG)
118 | asyncio.run(main())
119 |
120 | ```
121 |
122 | ## 程序架构
123 |
124 | 可以参考知乎的文章,知乎上的文章是随着项目开发,把思考过程都记录下来了:[知乎: 从零架构一个交易框架](https://zhuanlan.zhihu.com/p/268036337)
125 |
126 | ### RPC
127 | 因为这个项目的目标是为了服务各种不同的策略、不同和框架,所以基于不同的策略(日内、高频、日间)场景,提供了不一样的调用方式,而实现的方式主要基于 zeromq 来做的。
128 |
129 | 如果需要低延迟的话,可以以模块的方式加载服务,或者使用 zeromq 提供的 inproc 连接模式
130 |
131 | 如果需要本地调试方便,可以使用 ipc 的方式连接
132 |
133 | 如果需要分布式部署,可以使用 tcp 的方式连接
134 |
135 | 只要我们使用的不是模块的方式加载,我们都还会通过 MessagePack 的方式来序列化传输数据。在早期为了快速开发,就直接采用了 [zerorpc](http://www.zerorpc.io/) 来实现,但是 zerorpc 在性能上的问题比较大,所以到后期会重写一个 RPC 模块来替换掉
136 |
137 | ### 行情模块
138 |
139 | 第一个版本主要是做了以下行情的适配:
140 |
141 | 1. 美股:使用 pytdx 获取免费分钟数据
142 |
143 | 2. 外汇MT4:[dwx_zeromq_connector](https://github.com/darwinex/dwx-zeromq-connector)
144 |
145 | 3. 期货:计划采用 tqsdk 获取数据
146 |
147 |
148 | ### 交易模块
149 |
150 | 暂无,计划先做期货或者外汇
151 |
152 | ### 框架适配模块
153 |
154 | 暂无,计划先做 backtrader 的适配
155 |
156 | ## 关于 pytdx 的地址
157 |
158 | 可以参考 quantaxis 里的记录 [美股行情地址](https://github.com/QUANTAXIS/QUANTAXIS/blob/master/QUANTAXIS/QAUtil/QASetting.py#L364-L385)
159 |
160 | ## 关于外汇 MT4
161 |
162 | 因为 MT4 没有提供官方的 Api 接口,想要把里面的行情数据拿出来或者提交下单接口就只有两个方案:
163 |
164 | 1. 像pytdx一样抓包或者反编译
165 |
166 | 2. 用 MQL 语言写一个转发
167 |
168 | 好在现在已经有开源项目完成了后一种做法,这里就直接拿来用了:[dwx_zeromq_connector](https://github.com/darwinex/dwx-zeromq-connector),具体如何部署可以参考这个项目里面的说明。要注意的是我在原项目上做了一点修改,可以直接使用 extra 目录里的 mq4 代码,然后在 python 这边把原项目的多线程版本改为了 asyncio 的异步版本,可以直接使用。
169 |
170 | ## 其它
171 |
172 | 有什么好的想法和建议欢迎提issue,更新应该比较勤快,欢迎 star 和 fork 这个项目。知乎上也请大家多多支持和关注 [知乎专栏](https://www.zhihu.com/column/c_1177533241622593536)
173 |
--------------------------------------------------------------------------------
/examples/market_as_module.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from livetrader.market import (CachedMarket, DwxMarket, MarketService,
4 | TdxMarket)
5 |
6 |
7 | async def listern_to_latest_kline(kline_queue):
8 | print('get latest kline')
9 | for i in range(5):
10 | (symbol, kline) = await kline_queue.get()
11 | print("%s, %s" % (symbol, kline))
12 |
13 |
14 | async def get_kline_histories(service):
15 | print('get kline histories')
16 | histories = await service.get_kline_histories('FOREX.EURUSD', limit=100)
17 | print(len(histories))
18 |
19 |
20 | async def main():
21 | # Dwx 为外汇的行情获取,请参考文档设置 MT4
22 | market = DwxMarket(host='192.168.50.113')
23 | service = MarketService(market, ['FOREX.EURUSD'])
24 | kline_queue = service.start()
25 | await listern_to_latest_kline(kline_queue)
26 | await get_kline_histories(service)
27 | service.stop()
28 |
29 | if __name__ == "__main__":
30 | asyncio.run(main())
31 |
--------------------------------------------------------------------------------
/examples/market_as_standalone_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 |
4 | from livetrader.rpc import Client, MarketSubscriber
5 |
6 |
7 | class PrintSubscriber(MarketSubscriber):
8 |
9 | def on_kline(self, kline: dict):
10 | print('recv kline: %s' % kline, flush=True)
11 |
12 |
13 | def subscribe_kline(endpoint: str, symbol: str):
14 | subscriber = PrintSubscriber(symbol)
15 | subscriber.connect('ipc:///tmp/market')
16 | subscriber.run()
17 | return subscriber
18 |
19 |
20 | async def main():
21 | endpoint = "ipc:///tmp/market"
22 | symbol = 'US.BABA'
23 | subscriber = subscribe_kline(endpoint, symbol)
24 | await asyncio.sleep(5)
25 | subscriber.close()
26 |
27 | client = Client()
28 | client.connect(endpoint)
29 | histories = client.get_kline_histories(symbol, None, 100)
30 | print(len(histories))
31 | client.close()
32 |
33 |
34 | if __name__ == "__main__":
35 | logging.basicConfig(format=' %(name)s :: %(levelname)-8s :: %(message)s')
36 | root_logger = logging.getLogger()
37 | root_logger.setLevel(logging.DEBUG)
38 | asyncio.run(main())
39 |
--------------------------------------------------------------------------------
/examples/market_as_standalone_server.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from livetrader.market import CachedMarket, DwxMarket, MarketService, TdxMarket
4 | from livetrader.rpc import Server
5 |
6 |
7 | def create_server():
8 | market = CachedMarket(
9 | TdxMarket(
10 | host='59.175.238.38:7727'),
11 | mongodb_uri='mongodb://root:example@127.0.0.1:27017/?authMechanism=SCRAM-SHA-256')
12 | service = MarketService(market, ['US.BABA'])
13 | server = Server(service)
14 | server.bind('ipc:///tmp/market')
15 | return server
16 |
17 |
18 | if __name__ == "__main__":
19 | logging.basicConfig(format=' %(name)s :: %(levelname)-8s :: %(message)s')
20 | root_logger = logging.getLogger()
21 | root_logger.setLevel(logging.DEBUG)
22 | server = create_server()
23 | server.run()
24 |
--------------------------------------------------------------------------------
/extra/DWX_ZeroMQ_Server_v2.0.1_RC8.mq4:
--------------------------------------------------------------------------------
1 | //+--------------------------------------------------------------+
2 | //| DWX_ZeroMQ_Server_v2.0.1_RC8.mq4
3 | //| @author: Darwinex Labs (www.darwinex.com)
4 | //|
5 | //| Copyright (c) 2017-2020, Darwinex. All rights reserved.
6 | //|
7 | //| Licensed under the BSD 3-Clause License, you may not use this file except
8 | //| in compliance with the License.
9 | //|
10 | //| You may obtain a copy of the License at:
11 | //| https://opensource.org/licenses/BSD-3-Clause
12 | //+--------------------------------------------------------------+
13 | #property copyright "Copyright 2017-2020, Darwinex Labs."
14 | #property link "https://www.darwinex.com/"
15 | #property version "2.0.1"
16 | #property strict
17 |
18 | // Required: MQL-ZMQ from https://github.com/dingmaotu/mql-zmq
19 |
20 | #include
21 |
22 | extern string PROJECT_NAME = "DWX_ZeroMQ_MT4_Server";
23 | extern string ZEROMQ_PROTOCOL = "tcp";
24 | extern string HOSTNAME = "*";
25 | extern int PUSH_PORT = 32768;
26 | extern int PULL_PORT = 32769;
27 | extern int PUB_PORT = 32770;
28 | extern int MILLISECOND_TIMER = 1;
29 | extern int MILLISECOND_TIMER_PRICES = 500;
30 |
31 | extern string t0 = "--- Trading Parameters ---";
32 | extern int MaximumOrders = 1;
33 | extern double MaximumLotSize = 0.01;
34 | extern int MaximumSlippage = 3;
35 | extern bool DMA_MODE = true;
36 |
37 | /** Now, MarketData and MarketRates flags can change in real time, according with
38 | * registered symbols and instruments.
39 | */
40 | //extern string t1 = "--- ZeroMQ Configuration ---";
41 | bool Publish_MarketData = false;
42 | bool Publish_MarketRates = false;
43 |
44 | long lastUpdateMillis = GetTickCount();
45 |
46 |
47 |
48 | // Dynamic array initialized at OnInit(). Can be updated by TRACK_PRICES requests from client peers
49 | string Publish_Symbols[];
50 | string Publish_Symbols_LastTick[];
51 |
52 | // CREATE ZeroMQ Context
53 | Context context(PROJECT_NAME);
54 |
55 | // CREATE ZMQ_PUSH SOCKET
56 | Socket pushSocket(context, ZMQ_PUSH);
57 |
58 | // CREATE ZMQ_PULL SOCKET
59 | Socket pullSocket(context, ZMQ_PULL);
60 |
61 | // CREATE ZMQ_PUB SOCKET
62 | Socket pubSocket(context, ZMQ_PUB);
63 |
64 | // VARIABLES FOR LATER
65 | uchar _data[];
66 | ZmqMsg request;
67 |
68 | /**
69 | * Class definition for an specific instrument: the tuple (symbol,timeframe)
70 | */
71 | class Instrument {
72 | public:
73 |
74 | //--------------------------------------------------------------
75 | /** Instrument constructor */
76 | Instrument() { _symbol = ""; _name = ""; _timeframe = PERIOD_CURRENT; _last_pub_rate =0;}
77 |
78 | //--------------------------------------------------------------
79 | /** Getters */
80 | string symbol() { return _symbol; }
81 | ENUM_TIMEFRAMES timeframe() { return _timeframe; }
82 | string name() { return _name; }
83 | datetime getLastPublishTimestamp() { return _last_pub_rate; }
84 | /** Setters */
85 | void setLastPublishTimestamp(datetime tmstmp) { _last_pub_rate = tmstmp; }
86 |
87 | //--------------------------------------------------------------
88 | /** Setup instrument with symbol and timeframe descriptions
89 | * @param arg_symbol Symbol
90 | * @param arg_timeframe Timeframe
91 | */
92 | void setup(string arg_symbol, ENUM_TIMEFRAMES arg_timeframe) {
93 | _symbol = arg_symbol;
94 | _timeframe = arg_timeframe;
95 | _name = _symbol + "_" + GetTimeframeText(_timeframe);
96 | _last_pub_rate = 0;
97 | }
98 |
99 | //--------------------------------------------------------------
100 | /** Get last N MqlRates from this instrument (symbol-timeframe)
101 | * @param rates Receives last 'count' rates
102 | * @param count Number of requested rates
103 | * @return Number of returned rates
104 | */
105 | int GetRates(MqlRates& rates[], int count) {
106 | // ensures that symbol is setup
107 | if(StringLen(_symbol) > 0) {
108 | return CopyRates(_symbol, _timeframe, 0, count, rates);
109 | }
110 | return 0;
111 | }
112 |
113 | protected:
114 | string _name; //!< Instrument descriptive name
115 | string _symbol; //!< Symbol
116 | ENUM_TIMEFRAMES _timeframe; //!< Timeframe
117 | datetime _last_pub_rate; //!< Timestamp of the last published OHLC rate. Default = 0 (1 Jan 1970)
118 |
119 | };
120 |
121 | // Array of instruments whose rates will be published if Publish_MarketRates = True. It is initialized at OnInit() and
122 | // can be updated through TRACK_RATES request from client peers.
123 | Instrument Publish_Instruments[];
124 |
125 | //+------------------------------------------------------------------+
126 | //| Expert initialization function |
127 | //+------------------------------------------------------------------+
128 | int OnInit() {
129 |
130 | EventSetMillisecondTimer(MILLISECOND_TIMER); // Set Millisecond Timer to get client socket input
131 |
132 | context.setBlocky(false);
133 |
134 | // Send responses to PULL_PORT that client is listening on.
135 | if(!pushSocket.bind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PULL_PORT))) {
136 | Print("[PUSH] ####ERROR#### Binding MT4 Server to Socket on Port " + IntegerToString(PULL_PORT) + "..");
137 | return(INIT_FAILED);
138 | } else {
139 | Print("[PUSH] Binding MT4 Server to Socket on Port " + IntegerToString(PULL_PORT) + "..");
140 | pushSocket.setSendHighWaterMark(1);
141 | pushSocket.setLinger(0);
142 | }
143 |
144 | // Receive commands from PUSH_PORT that client is sending to.
145 | if(!pullSocket.bind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUSH_PORT))) {
146 | Print("[PULL] ####ERROR#### Binding MT4 Server to Socket on Port " + IntegerToString(PUSH_PORT) + "..");
147 | return(INIT_FAILED);
148 | } else {
149 | Print("[PULL] Binding MT4 Server to Socket on Port " + IntegerToString(PUSH_PORT) + "..");
150 | pullSocket.setReceiveHighWaterMark(1);
151 | pullSocket.setLinger(0);
152 | }
153 |
154 | // Send new market data to PUB_PORT that client is subscribed to.
155 | if(!pubSocket.bind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUB_PORT))) {
156 | Print("[PUB] ####ERROR#### Binding MT4 Server to Socket on Port " + IntegerToString(PUB_PORT) + "..");
157 | return(INIT_FAILED);
158 | } else {
159 | Print("[PUB] Binding MT4 Server to Socket on Port " + IntegerToString(PUB_PORT) + "..");
160 | pubSocket.setSendHighWaterMark(1);
161 | pubSocket.setLinger(0);
162 | }
163 | return(INIT_SUCCEEDED);
164 | }
165 | //+------------------------------------------------------------------+
166 | //| Expert deinitialization function |
167 | //+------------------------------------------------------------------+
168 | void OnDeinit(const int reason) {
169 |
170 | Print("[PUSH] Unbinding MT4 Server from Socket on Port " + IntegerToString(PULL_PORT) + "..");
171 | pushSocket.unbind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PULL_PORT));
172 | pushSocket.disconnect(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PULL_PORT));
173 |
174 | Print("[PULL] Unbinding MT4 Server from Socket on Port " + IntegerToString(PUSH_PORT) + "..");
175 | pullSocket.unbind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUSH_PORT));
176 | pullSocket.disconnect(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUSH_PORT));
177 |
178 | if (Publish_MarketData == true || Publish_MarketRates == true) {
179 | Print("[PUB] Unbinding MT4 Server from Socket on Port " + IntegerToString(PUB_PORT) + "..");
180 | pubSocket.unbind(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUB_PORT));
181 | pubSocket.disconnect(StringFormat("%s://%s:%d", ZEROMQ_PROTOCOL, HOSTNAME, PUB_PORT));
182 | }
183 |
184 | // Shutdown ZeroMQ Context
185 | context.shutdown();
186 | context.destroy(0);
187 |
188 | EventKillTimer();
189 | }
190 |
191 | //+------------------------------------------------------------------+
192 | //| Expert tick function |
193 | //+------------------------------------------------------------------+
194 | void OnTick() {
195 | /*
196 | Use this OnTick() function to send market data to subscribed client.
197 | */
198 | lastUpdateMillis = GetTickCount();
199 |
200 | if(CheckServerStatus() == true) {
201 | // Python clients can subscribe to a price feed for each tracked symbol
202 | if(Publish_MarketData == true) {
203 |
204 | for(int s = 0; s < ArraySize(Publish_Symbols); s++) {
205 |
206 | string _tick = GetBidAsk(Publish_Symbols[s]);
207 | // only update if bid or ask changed.
208 | if (StringCompare(Publish_Symbols_LastTick[s], _tick) == 0) continue;
209 | Publish_Symbols_LastTick[s] = _tick;
210 | // publish: topic=symbol msg=tick_data
211 | ZmqMsg reply(StringFormat("%s %s", Publish_Symbols[s], _tick));
212 | Print("Sending PRICE [" + reply.getData() + "] to PUB Socket");
213 | if(!pubSocket.send(reply, true)) {
214 | Print("###ERROR### Sending price");
215 | }
216 | }
217 | }
218 |
219 | // Python clients can also subscribe to a rates feed for each tracked instrument
220 | if(Publish_MarketRates == true) {
221 | for(int s = 0; s < ArraySize(Publish_Instruments); s++) {
222 | MqlRates curr_rate[];
223 | int count = Publish_Instruments[s].GetRates(curr_rate, 1);
224 | // if last rate is returned and its timestamp is greater than the last published...
225 | if(count > 0 && curr_rate[0].time > Publish_Instruments[s].getLastPublishTimestamp()) {
226 | // then send a new pub message with this new rate
227 | string _rates = StringFormat("%u;%f;%f;%f;%f;%d;%d;%d",
228 | curr_rate[0].time,
229 | curr_rate[0].open,
230 | curr_rate[0].high,
231 | curr_rate[0].low,
232 | curr_rate[0].close,
233 | curr_rate[0].tick_volume,
234 | curr_rate[0].spread,
235 | curr_rate[0].real_volume);
236 | ZmqMsg reply(StringFormat("%s %s", Publish_Instruments[s].name(), _rates));
237 | Print("Sending Rates @"+TimeToStr(curr_rate[0].time) + " [" + reply.getData() + "] to PUB Socket");
238 | if(!pubSocket.send(reply, true)) {
239 | Print("###ERROR### Sending rate");
240 | }
241 | // updates the timestamp
242 | // Publish_Instruments[s].setLastPublishTimestamp(curr_rate[0].time);
243 |
244 | }
245 | }
246 | }
247 | }
248 | }
249 |
250 | //+------------------------------------------------------------------+
251 | //| Expert timer function |
252 | //+------------------------------------------------------------------+
253 | void OnTimer() {
254 |
255 | /*
256 | Use this OnTimer() function to get and respond to commands
257 | */
258 |
259 | if(CheckServerStatus() == true) {
260 | // Get client's response, but don't block.
261 | pullSocket.recv(request, true);
262 |
263 | if (request.size() > 0) {
264 | // Wait
265 | // pullSocket.recv(request,false);
266 |
267 | // MessageHandler() should go here.
268 | ZmqMsg reply = MessageHandler(request);
269 |
270 | // Send response, and block
271 | // pushSocket.send(reply);
272 |
273 | // Send response, but don't block
274 | if(!pushSocket.send(reply, true)) {
275 | Print("###ERROR### Sending message");
276 | }
277 | }
278 |
279 | // update prices regularly in case there was no tick within X milliseconds (for non-chart symbols).
280 | if (GetTickCount() >= lastUpdateMillis + MILLISECOND_TIMER_PRICES) OnTick();
281 | }
282 | }
283 | //+------------------------------------------------------------------+
284 |
285 | ZmqMsg MessageHandler(ZmqMsg &_request) {
286 |
287 | // Output object
288 | ZmqMsg reply;
289 |
290 | // Message components for later.
291 | string components[11];
292 |
293 | if(_request.size() > 0) {
294 |
295 | // Get data from request
296 | ArrayResize(_data, _request.size());
297 | _request.getData(_data);
298 | string dataStr = CharArrayToString(_data);
299 |
300 | // Process data
301 | ParseZmqMessage(dataStr, components);
302 |
303 | // Interpret data
304 | InterpretZmqMessage(pushSocket, components);
305 |
306 | } else {
307 | // NO DATA RECEIVED
308 | }
309 |
310 | return(reply);
311 | }
312 |
313 | //+------------------------------------------------------------------+
314 | // Interpret Zmq Message and perform actions
315 | void InterpretZmqMessage(Socket &pSocket, string &compArray[]) {
316 |
317 | // Message Structures:
318 |
319 | // 1) Trading
320 | // TRADE|ACTION|TYPE|SYMBOL|PRICE|SL|TP|COMMENT|TICKET
321 | // e.g. TRADE|OPEN|1|EURUSD|0|50|50|R-to-MetaTrader4|12345678
322 |
323 | // The 12345678 at the end is the ticket ID, for MODIFY and CLOSE.
324 |
325 | // 2) Data Requests
326 |
327 | // 2.1) RATES|SYMBOL -> Returns Current Bid/Ask
328 |
329 | // 2.2) DATA|SYMBOL|TIMEFRAME|START_DATETIME|END_DATETIME
330 |
331 | // 2.3) HIST|SYMBOL|TIMEFRAME|START_DATETIME|END_DATETIME
332 |
333 | // 3) Instruments configuration
334 |
335 | // 3.1) TRACK_PRICES|SYMBOL_1|SYMBOL_2|...|SYMBOL_N -> List of symbols to receive real-time price updates (bid-ask)
336 |
337 | // 3.2) TRACK_RATES|INSTRUMENT_1|INSTRUMENT_2|...|INSTRUMENT_N -> List of instruments to receive OHLC rates
338 | // Note: Instruments are bilt with format: SYMBOL_TIMEFRAME for example:
339 | // Symbol: EURUSD, Timeframe: PERIOD_M1 ----> Instrument = "EURUSD_M1"
340 | // Symbol: GDAXI, Timeframe: PERIOD_H4 ----> Instrument = "GDAXI_H4"
341 |
342 | // NOTE: datetime has format: D'2015.01.01 00:00'
343 |
344 | /*
345 | compArray[0] = TRADE, HIST, TRACK_PRICES, TRACK_RATES
346 | If HIST, TRACK_PRICES, TRACK_RATES -> compArray[1] = Symbol
347 |
348 | If TRADE ->
349 | compArray[0] = TRADE
350 | compArray[1] = ACTION (e.g. OPEN, MODIFY, CLOSE)
351 | compArray[2] = TYPE (e.g. OP_BUY, OP_SELL, etc - only used when ACTION=OPEN)
352 |
353 | // ORDER TYPES:
354 | // https://docs.mql4.com/constants/tradingconstants/orderproperties
355 |
356 | // OP_BUY = 0
357 | // OP_SELL = 1
358 | // OP_BUYLIMIT = 2
359 | // OP_SELLLIMIT = 3
360 | // OP_BUYSTOP = 4
361 | // OP_SELLSTOP = 5
362 |
363 | compArray[3] = Symbol (e.g. EURUSD, etc.)
364 | compArray[4] = Open/Close Price (ignored if ACTION = MODIFY)
365 | compArray[5] = SL
366 | compArray[6] = TP
367 | compArray[7] = Trade Comment
368 | compArray[8] = Lots
369 | compArray[9] = Magic Number
370 | compArray[10] = Ticket Number (MODIFY/CLOSE)
371 | */
372 |
373 | int switch_action = 0;
374 |
375 | /* 02-08-2019 10:41 CEST - HEARTBEAT */
376 | if(compArray[0] == "HEARTBEAT")
377 | InformPullClient(pSocket, "{'_action': 'heartbeat', '_response': 'loud and clear!'}");
378 |
379 | /* Process Messages */
380 | if(compArray[0] == "TRADE" && compArray[1] == "OPEN")
381 | switch_action = 1;
382 | if(compArray[0] == "TRADE" && compArray[1] == "MODIFY")
383 | switch_action = 2;
384 | if(compArray[0] == "TRADE" && compArray[1] == "CLOSE")
385 | switch_action = 3;
386 | if(compArray[0] == "TRADE" && compArray[1] == "CLOSE_PARTIAL")
387 | switch_action = 4;
388 | if(compArray[0] == "TRADE" && compArray[1] == "CLOSE_MAGIC")
389 | switch_action = 5;
390 | if(compArray[0] == "TRADE" && compArray[1] == "CLOSE_ALL")
391 | switch_action = 6;
392 | if(compArray[0] == "TRADE" && compArray[1] == "GET_OPEN_TRADES")
393 | switch_action = 7;
394 | if(compArray[0] == "HIST")
395 | switch_action = 8;
396 | if(compArray[0] == "TRACK_PRICES")
397 | switch_action = 9;
398 | if(compArray[0] == "TRACK_RATES")
399 | switch_action = 10;
400 |
401 | // IMPORTANT: when adding new functions, also increase the max switch_action in CheckOpsStatus()!
402 |
403 | /* Setup processing variables */
404 | string zmq_ret = "";
405 | string ret = "";
406 | int ticket = -1;
407 | bool ans = false;
408 |
409 | /****************************
410 | * PERFORM SOME CHECKS HERE *
411 | ****************************/
412 | if (CheckOpsStatus(pSocket, switch_action) == true) {
413 |
414 | switch(switch_action) {
415 | case 1: // OPEN TRADE
416 |
417 | zmq_ret = "{";
418 |
419 | // Function definition:
420 | ticket = DWX_OpenOrder(compArray[3], StrToInteger(compArray[2]), StrToDouble(compArray[8]), StrToDouble(compArray[4]),
421 | StrToInteger(compArray[5]), StrToInteger(compArray[6]), compArray[7], StrToInteger(compArray[9]), zmq_ret);
422 |
423 | // Send TICKET back as JSON
424 | InformPullClient(pSocket, zmq_ret + "}");
425 |
426 | break;
427 |
428 |
429 | case 2: // MODIFY SL/TP
430 |
431 | zmq_ret = "{'_action': 'MODIFY'";
432 |
433 | // Function definition:
434 | ans = DWX_ModifyOrder(StrToInteger(compArray[10]), StrToDouble(compArray[4]), StrToDouble(compArray[5]), StrToDouble(compArray[6]), 3, zmq_ret);
435 |
436 | InformPullClient(pSocket, zmq_ret + "}");
437 |
438 | break;
439 |
440 | case 3: // CLOSE TRADE
441 |
442 | zmq_ret = "{";
443 |
444 | // IMPLEMENT CLOSE TRADE LOGIC HERE
445 | DWX_CloseOrder_Ticket(StrToInteger(compArray[10]), zmq_ret);
446 |
447 | InformPullClient(pSocket, zmq_ret + "}");
448 |
449 | break;
450 |
451 | case 4: // CLOSE PARTIAL
452 |
453 | zmq_ret = "{";
454 |
455 | ans = DWX_ClosePartial(StrToDouble(compArray[8]), zmq_ret, StrToInteger(compArray[10]), true);
456 |
457 | InformPullClient(pSocket, zmq_ret + "}");
458 |
459 | break;
460 |
461 | case 5: // CLOSE MAGIC
462 |
463 | zmq_ret = "{";
464 |
465 | DWX_CloseOrder_Magic(StrToInteger(compArray[9]), zmq_ret);
466 |
467 | InformPullClient(pSocket, zmq_ret + "}");
468 |
469 | break;
470 |
471 | case 6: // CLOSE ALL ORDERS
472 |
473 | zmq_ret = "{";
474 |
475 | DWX_CloseAllOrders(zmq_ret);
476 |
477 | InformPullClient(pSocket, zmq_ret + "}");
478 |
479 | break;
480 |
481 | case 7: // GET OPEN ORDERS
482 |
483 | zmq_ret = "{";
484 |
485 | DWX_GetOpenOrders(zmq_ret);
486 |
487 | InformPullClient(pSocket, zmq_ret + "}");
488 |
489 | break;
490 |
491 | case 8: // HIST REQUEST
492 |
493 | zmq_ret = "{";
494 |
495 | DWX_GetHist(compArray, zmq_ret);
496 |
497 | InformPullClient(pSocket, zmq_ret + "}");
498 |
499 | break;
500 |
501 | case 9: // SETUP LIST OF SYMBOLS TO TRACK PRICES
502 |
503 | zmq_ret = "{";
504 |
505 | DWX_SetSymbolList(compArray, zmq_ret);
506 |
507 | InformPullClient(pSocket, zmq_ret + "}");
508 |
509 | break;
510 |
511 | case 10: // SETUP LIST OF INSTRUMENTS TO TRACK RATES
512 |
513 | zmq_ret = "{";
514 |
515 | DWX_SetInstrumentList(compArray, zmq_ret);
516 |
517 | InformPullClient(pSocket, zmq_ret + "}");
518 |
519 | break;
520 |
521 | // if a case is added, also change max switch_action in CheckOpsStatus()!
522 |
523 | default:
524 | break;
525 | }
526 | }
527 | }
528 |
529 | // Check if operations are permitted
530 | bool CheckOpsStatus(Socket &pSocket, int switch_action) {
531 |
532 | if (switch_action >= 1 && switch_action <= 10) {
533 |
534 | if (!IsTradeAllowed()) {
535 | InformPullClient(pSocket, "{'_response': 'TRADING_IS_NOT_ALLOWED__ABORTED_COMMAND'}");
536 | return(false);
537 | }
538 | else if (!IsExpertEnabled()) {
539 | InformPullClient(pSocket, "{'_response': 'EA_IS_DISABLED__ABORTED_COMMAND'}");
540 | return(false);
541 | }
542 | else if (IsTradeContextBusy()) {
543 | InformPullClient(pSocket, "{'_response': 'TRADE_CONTEXT_BUSY__ABORTED_COMMAND'}");
544 | return(false);
545 | }
546 | else if (!IsDllsAllowed()) {
547 | InformPullClient(pSocket, "{'_response': 'DLLS_DISABLED__ABORTED_COMMAND'}");
548 | return(false);
549 | }
550 | else if (!IsLibrariesAllowed()) {
551 | InformPullClient(pSocket, "{'_response': 'LIBS_DISABLED__ABORTED_COMMAND'}");
552 | return(false);
553 | }
554 | else if (!IsConnected()) {
555 | InformPullClient(pSocket, "{'_response': 'NO_BROKER_CONNECTION__ABORTED_COMMAND'}");
556 | return(false);
557 | }
558 | }
559 |
560 | return(true);
561 | }
562 |
563 | // Parse Zmq Message
564 | void ParseZmqMessage(string& message, string& retArray[]) {
565 |
566 | //Print("Parsing: " + message);
567 |
568 | string sep = ";";
569 | ushort u_sep = StringGetCharacter(sep,0);
570 |
571 | int splits = StringSplit(message, u_sep, retArray);
572 |
573 | /*
574 | for(int i = 0; i < splits; i++) {
575 | Print(IntegerToString(i) + ") " + retArray[i]);
576 | }
577 | */
578 | }
579 |
580 | //+------------------------------------------------------------------+
581 | // Generate string for Bid/Ask by symbol
582 | string GetBidAsk(string symbol) {
583 |
584 | MqlTick last_tick;
585 |
586 | if(SymbolInfoTick(symbol,last_tick)) {
587 | return(StringFormat("%f;%f", last_tick.bid, last_tick.ask));
588 | }
589 |
590 | // Default
591 | return "";
592 | }
593 |
594 | //+------------------------------------------------------------------+
595 | // Get historic for request datetime range
596 | void DWX_GetHist(string& compArray[], string& zmq_ret) {
597 |
598 | // Format: HIST|SYMBOL|TIMEFRAME|START_DATETIME|END_DATETIME
599 |
600 | string _symbol = compArray[1];
601 | ENUM_TIMEFRAMES _timeframe = (ENUM_TIMEFRAMES)StrToInteger(compArray[2]);
602 |
603 | MqlRates rates_array[];
604 |
605 | // Get prices
606 | int rates_count = 0;
607 |
608 | // Handling ERR_HISTORY_WILL_UPDATED (4066) and ERR_NO_HISTORY_DATA (4073) errors.
609 | // For non-chart symbols and time frames MT4 often needs a few requests until the data is available.
610 | // But even after 10 requests it can happen that it is not available. So it is best to have the charts open.
611 | for (int i=0; i<10; i++) {
612 | rates_count = CopyRates(_symbol,
613 | _timeframe, StrToTime(compArray[3]),
614 | StrToTime(compArray[4]), rates_array);
615 | int errorCode = GetLastError();
616 | // Print("errorCode: ", errorCode);
617 | if (rates_count > 0 || (errorCode != 4066 && errorCode != 4073)) break;
618 | Sleep(200);
619 | }
620 |
621 | zmq_ret = zmq_ret + "'_action': 'HIST', '_symbol': '" + _symbol+ "_" + GetTimeframeText(_timeframe) + "'";
622 |
623 | // if data then forms response as json:
624 | // {'_action: 'HIST',
625 | // '_data':[{'time': 'YYYY:MM:DD,HH:MM:SS', 'open':0.0, 'high':0.0, 'low':0.0, 'close':0.0, 'tick_volume:0, 'spread':0, 'real_volume':0},
626 | // {...},
627 | // ...
628 | // ]
629 | // }
630 | if (rates_count > 0) {
631 |
632 | zmq_ret = zmq_ret + ", '_data': [";
633 |
634 | // Construct string of rates and send to PULL client.
635 | for(int i = 0; i < rates_count; i++ ) {
636 |
637 | if(i == 0)
638 | zmq_ret = zmq_ret + "{'time':'" + TimeToString(rates_array[i].time) + "', 'open':" + DoubleToString(rates_array[i].open) + ", 'high':" + DoubleToString(rates_array[i].high) + ", 'low':" + DoubleToString(rates_array[i].low) + ", 'close':" + DoubleToString(rates_array[i].close) + ", 'tick_volume':" + IntegerToString(rates_array[i].tick_volume) + ", 'spread':" + IntegerToString(rates_array[i].spread) + ", 'real_volume':" + IntegerToString(rates_array[i].real_volume) + "}";
639 | else
640 | zmq_ret = zmq_ret + ", {'time':'" + TimeToString(rates_array[i].time) + "', 'open':" + DoubleToString(rates_array[i].open) + ", 'high':" + DoubleToString(rates_array[i].high) + ", 'low':" + DoubleToString(rates_array[i].low) + ", 'close':" + DoubleToString(rates_array[i].close) + ", 'tick_volume':" + IntegerToString(rates_array[i].tick_volume) + ", 'spread':" + IntegerToString(rates_array[i].spread) + ", 'real_volume':" + IntegerToString(rates_array[i].real_volume) + "}";
641 |
642 | }
643 |
644 | zmq_ret = zmq_ret + "]";
645 |
646 | }
647 | // if NO data then forms response as json:
648 | // {'_action: 'HIST',
649 | // '_response': 'NOT_AVAILABLE'
650 | // }
651 | else {
652 | zmq_ret = zmq_ret + ", " + "'_response': 'NOT_AVAILABLE'";
653 | }
654 | }
655 |
656 | //+------------------------------------------------------------------+
657 | // Set list of symbols to get real-time price data
658 | void DWX_SetSymbolList(string& compArray[], string& zmq_ret) {
659 |
660 | zmq_ret = zmq_ret + "'_action': 'TRACK_PRICES'";
661 |
662 | // Format: TRACK_PRICES|SYMBOL_1|SYMBOL_2|...|SYMBOL_N
663 | string result = "Tracking PRICES from";
664 | string errorSymbols = "";
665 | int _num_symbols = ArraySize(compArray) - 1;
666 | if(_num_symbols > 0) {
667 | for(int s=0; s<_num_symbols; s++) {
668 | if (SymbolSelect(compArray[s+1], true)) {
669 | ArrayResize(Publish_Symbols, s+1);
670 | ArrayResize(Publish_Symbols_LastTick, s+1);
671 | Publish_Symbols[s] = compArray[s+1];
672 | result += " " + Publish_Symbols[s];
673 | } else {
674 | errorSymbols += "'" + compArray[s+1] + "', ";
675 | }
676 | }
677 | if (StringLen(errorSymbols) > 0)
678 | errorSymbols = "[" + StringSubstr(errorSymbols, 0, StringLen(errorSymbols)-2) + "]";
679 | else
680 | errorSymbols = "[]";
681 | zmq_ret = zmq_ret + ", '_data': {'symbol_count':" + IntegerToString(_num_symbols) + ", 'error_symbols':" + errorSymbols + "}";
682 | Publish_MarketData = true;
683 | } else {
684 | Publish_MarketData = false;
685 | ArrayResize(Publish_Symbols, 1);
686 | ArrayResize(Publish_Symbols_LastTick, 1);
687 | zmq_ret = zmq_ret + ", '_data': {'symbol_count': 0}";
688 | result += " NONE";
689 | }
690 | Print(result);
691 | }
692 |
693 |
694 | //+------------------------------------------------------------------+
695 | // Set list of instruments to get OHLC rates
696 | void DWX_SetInstrumentList(string& compArray[], string& zmq_ret) {
697 |
698 | zmq_ret = zmq_ret + "'_action': 'TRACK_RATES'";
699 |
700 | printArray(compArray);
701 |
702 | // Format: TRACK_RATES|SYMBOL_1|TIMEFRAME_1|SYMBOL_2|TIMEFRAME_2|...|SYMBOL_N|TIMEFRAME_N
703 | string result = "Tracking RATES from";
704 | string errorSymbols = "";
705 | int _num_instruments = (ArraySize(compArray) - 1)/2;
706 | if(_num_instruments > 0) {
707 | for(int s=0; s<_num_instruments; s++) {
708 | if (SymbolSelect(compArray[(2*s)+1], true)) {
709 | ArrayResize(Publish_Instruments, s+1);
710 | Publish_Instruments[s].setup(compArray[(2*s)+1], (ENUM_TIMEFRAMES)StrToInteger(compArray[(2*s)+2]));
711 | result += " " + Publish_Instruments[s].name();
712 | } else {
713 | errorSymbols += "'" + compArray[(2*s)+1] + "', ";
714 | }
715 | }
716 | if (StringLen(errorSymbols) > 0)
717 | errorSymbols = "[" + StringSubstr(errorSymbols, 0, StringLen(errorSymbols)-2) + "]";
718 | else
719 | errorSymbols = "[]";
720 | zmq_ret = zmq_ret + ", '_data': {'instrument_count':" + IntegerToString(_num_instruments) + ", 'error_symbols':" + errorSymbols + "}";
721 | Publish_MarketRates = true;
722 | } else {
723 | Publish_MarketRates = false;
724 | ArrayResize(Publish_Instruments, 1);
725 | zmq_ret = zmq_ret + ", '_data': {'instrument_count': 0}";
726 | result += " NONE";
727 | }
728 | Print(result);
729 | }
730 |
731 |
732 | //+------------------------------------------------------------------+
733 | // Get Timeframe from text
734 | string GetTimeframeText(ENUM_TIMEFRAMES tf) {
735 | // Standard timeframes
736 | switch(tf) {
737 | case PERIOD_M1: return "M1";
738 | case PERIOD_M5: return "M5";
739 | case PERIOD_M15: return "M15";
740 | case PERIOD_M30: return "M30";
741 | case PERIOD_H1: return "H1";
742 | case PERIOD_H4: return "H4";
743 | case PERIOD_D1: return "D1";
744 | case PERIOD_W1: return "W1";
745 | case PERIOD_MN1: return "MN1";
746 | default: return "UNKNOWN";
747 | }
748 | }
749 |
750 | // Inform Client
751 | void InformPullClient(Socket& pSocket, string message) {
752 |
753 | ZmqMsg pushReply(message);
754 |
755 | pSocket.send(pushReply,true); // NON-BLOCKING
756 |
757 | }
758 |
759 | //+------------------------------------------------------------------+
760 |
761 | // OPEN NEW ORDER
762 | int DWX_OpenOrder(string _symbol, int _type, double _lots, double _price, double _SL, double _TP, string _comment, int _magic, string &zmq_ret) {
763 |
764 | int ticket, error;
765 |
766 | zmq_ret = zmq_ret + "'_action': 'EXECUTION'";
767 |
768 | if(_lots > MaximumLotSize) {
769 | zmq_ret = zmq_ret + ", " + "'_response': 'LOT_SIZE_ERROR', 'response_value': 'MAX_LOT_SIZE_EXCEEDED'";
770 | return(-1);
771 | }
772 |
773 | if(OrdersTotal() >= MaximumOrders) {
774 | zmq_ret = zmq_ret + ", " + "'_response': 'NUM_ORDERS_ERROR', 'response_value': 'MAX_NUMBER_OF_ORDERS_EXCEEDED'";
775 | return(-1);
776 | }
777 |
778 | if (_type == OP_BUY)
779 | _price = MarketInfo(_symbol, MODE_ASK);
780 | else if (_type == OP_SELL)
781 | _price = MarketInfo(_symbol, MODE_BID);
782 |
783 |
784 | double sl = 0.0;
785 | double tp = 0.0;
786 |
787 | if(!DMA_MODE) {
788 | int dir_flag = -1;
789 |
790 | if (_type == OP_BUY || _type == OP_BUYLIMIT || _type == OP_BUYSTOP)
791 | dir_flag = 1;
792 |
793 | double vpoint = MarketInfo(_symbol, MODE_POINT);
794 | int vdigits = (int)MarketInfo(_symbol, MODE_DIGITS);
795 | sl = NormalizeDouble(_price-_SL*dir_flag*vpoint, vdigits);
796 | tp = NormalizeDouble(_price+_TP*dir_flag*vpoint, vdigits);
797 | }
798 |
799 | if(_symbol == "NULL") {
800 | ticket = OrderSend(Symbol(), _type, _lots, _price, MaximumSlippage, sl, tp, _comment, _magic);
801 | } else {
802 | ticket = OrderSend(_symbol, _type, _lots, _price, MaximumSlippage, sl, tp, _comment, _magic);
803 | }
804 | if(ticket < 0) {
805 | // Failure
806 | error = GetLastError();
807 | zmq_ret = zmq_ret + ", " + "'_response': '" + IntegerToString(error) + "', 'response_value': '" + ErrorDescription(error) + "'";
808 | return(-1*error);
809 | }
810 |
811 | int tmpRet = OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES);
812 |
813 | zmq_ret = zmq_ret + ", " + "'_magic': " + IntegerToString(_magic) + ", '_ticket': " + IntegerToString(OrderTicket()) + ", '_open_time': '" + TimeToStr(OrderOpenTime(),TIME_DATE|TIME_SECONDS) + "', '_open_price': " + DoubleToString(OrderOpenPrice());
814 |
815 | if(DMA_MODE) {
816 |
817 | int retries = 3;
818 | while(true) {
819 | retries--;
820 | if(retries < 0) return(0);
821 |
822 | if((_SL == 0 && _TP == 0) || (OrderStopLoss() == _SL && OrderTakeProfit() == _TP)) {
823 | return(ticket);
824 | }
825 |
826 | if(DWX_IsTradeAllowed(30, zmq_ret) == 1) {
827 | if(DWX_ModifyOrder(ticket, _price, _SL, _TP, retries, zmq_ret)) {
828 | return(ticket);
829 | }
830 | if(retries == 0) {
831 | zmq_ret = zmq_ret + ", '_response': 'ERROR_SETTING_SL_TP'";
832 | return(-11111);
833 | }
834 | }
835 |
836 | Sleep(MILLISECOND_TIMER);
837 | }
838 |
839 | zmq_ret = zmq_ret + ", '_response': 'ERROR_SETTING_SL_TP'";
840 | zmq_ret = zmq_ret + "}";
841 | return(-1);
842 | }
843 |
844 | // Send zmq_ret to Python Client
845 | zmq_ret = zmq_ret + "}";
846 |
847 | return(ticket);
848 | }
849 |
850 | //+------------------------------------------------------------------+
851 | // SET SL/TP
852 | bool DWX_ModifyOrder(int ticket, double _price, double _SL, double _TP, int retries, string &zmq_ret) {
853 |
854 | if (OrderSelect(ticket, SELECT_BY_TICKET) == true) {
855 |
856 | if (OrderType() == OP_BUY || OrderType() == OP_SELL || _price == 0.0)
857 | _price = OrderOpenPrice();
858 |
859 | int dir_flag = -1;
860 |
861 | if (OrderType() == OP_BUY || OrderType() == OP_BUYLIMIT || OrderType() == OP_BUYSTOP)
862 | dir_flag = 1;
863 |
864 | double vpoint = MarketInfo(OrderSymbol(), MODE_POINT);
865 | int vdigits = (int)MarketInfo(OrderSymbol(), MODE_DIGITS);
866 | double mSL = NormalizeDouble(_price-_SL*dir_flag*vpoint, vdigits);
867 | double mTP = NormalizeDouble(_price+_TP*dir_flag*vpoint, vdigits);
868 |
869 | if(OrderModify(ticket, _price, mSL, mTP, 0, 0)) {
870 | zmq_ret = zmq_ret + ", '_sl': " + DoubleToString(mSL) + ", '_tp': " + DoubleToString(mTP);
871 | return(true);
872 | } else {
873 | int error = GetLastError();
874 | zmq_ret = zmq_ret + ", '_response': '" + IntegerToString(error) + "', '_response_value': '" + ErrorDescription(error) + "', '_sl_attempted': " + DoubleToString(mSL, vdigits) + ", '_tp_attempted': " + DoubleToString(mTP, vdigits);
875 |
876 | if(retries == 0) {
877 | RefreshRates();
878 | DWX_CloseAtMarket(-1, zmq_ret);
879 | }
880 |
881 | return(false);
882 | }
883 | } else {
884 | zmq_ret = zmq_ret + ", '_response': 'NOT_FOUND'";
885 | }
886 |
887 | return(false);
888 | }
889 |
890 | //+------------------------------------------------------------------+
891 | // CLOSE AT MARKET
892 | bool DWX_CloseAtMarket(double size, string &zmq_ret) {
893 |
894 | int error;
895 |
896 | int retries = 3;
897 | while(true) {
898 | retries--;
899 | if(retries < 0) return(false);
900 |
901 | if(DWX_IsTradeAllowed(30, zmq_ret) == 1) {
902 | if(DWX_ClosePartial(size, zmq_ret)) {
903 | // trade successfuly closed
904 | return(true);
905 | } else {
906 | error = GetLastError();
907 | zmq_ret = zmq_ret + ", '_response': '" + IntegerToString(error) + "', '_response_value': '" + ErrorDescription(error) + "'";
908 | }
909 | }
910 |
911 | }
912 | return(false);
913 | }
914 |
915 | //+------------------------------------------------------------------+
916 | // CLOSE PARTIAL SIZE
917 | bool DWX_ClosePartial(double size, string &zmq_ret, int ticket=0, bool externCall=false) {
918 |
919 | if(OrderType() != OP_BUY && OrderType() != OP_SELL) {
920 | return(true);
921 | }
922 |
923 | int error;
924 | bool close_ret = False;
925 |
926 | // If the function is called directly, setup init() JSON here and get OrderSelect.
927 | if(ticket != 0 || externCall) {
928 | zmq_ret = zmq_ret + "'_action': 'CLOSE', '_ticket': " + IntegerToString(ticket);
929 |
930 | if (OrderSelect(ticket, SELECT_BY_TICKET)) {
931 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_PARTIAL'";
932 | } else {
933 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_PARTIAL_FAILED'";
934 | return(false);
935 | }
936 | }
937 |
938 | RefreshRates();
939 | double priceCP = OrderClosePrice();
940 |
941 | if(size < 0.01 || size > OrderLots()) {
942 | size = OrderLots();
943 | }
944 | close_ret = OrderClose(OrderTicket(), size, priceCP, MaximumSlippage);
945 |
946 | if (close_ret == true) {
947 | zmq_ret = zmq_ret + ", '_close_price': " + DoubleToString(priceCP) + ", '_close_lots': " + DoubleToString(size);
948 | } else {
949 | error = GetLastError();
950 | zmq_ret = zmq_ret + ", '_response': '" + IntegerToString(error) + "', '_response_value': '" + ErrorDescription(error) + "'";
951 | }
952 |
953 | return(close_ret);
954 | }
955 |
956 | //+------------------------------------------------------------------+
957 | // CLOSE ORDER (by Magic Number)
958 | void DWX_CloseOrder_Magic(int _magic, string &zmq_ret) {
959 |
960 | bool found = false;
961 |
962 | zmq_ret = zmq_ret + "'_action': 'CLOSE_ALL_MAGIC'";
963 | zmq_ret = zmq_ret + ", '_magic': " + IntegerToString(_magic);
964 |
965 | zmq_ret = zmq_ret + ", '_responses': {";
966 |
967 | for(int i=OrdersTotal()-1; i >= 0; i--) {
968 | if (OrderSelect(i,SELECT_BY_POS)==true && OrderMagicNumber() == _magic) {
969 | found = true;
970 |
971 | zmq_ret = zmq_ret + IntegerToString(OrderTicket()) + ": {'_symbol':'" + OrderSymbol() + "'";
972 |
973 | if(OrderType() == OP_BUY || OrderType() == OP_SELL) {
974 | DWX_CloseAtMarket(-1, zmq_ret);
975 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_MARKET'";
976 |
977 | if (i != 0)
978 | zmq_ret = zmq_ret + "}, ";
979 | else
980 | zmq_ret = zmq_ret + "}";
981 |
982 | } else {
983 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_PENDING'";
984 |
985 | if (i != 0)
986 | zmq_ret = zmq_ret + "}, ";
987 | else
988 | zmq_ret = zmq_ret + "}";
989 |
990 | int tmpRet = OrderDelete(OrderTicket());
991 | }
992 | }
993 | }
994 |
995 | zmq_ret = zmq_ret + "}";
996 |
997 | if(found == false) {
998 | zmq_ret = zmq_ret + ", '_response': 'NOT_FOUND'";
999 | }
1000 | else {
1001 | zmq_ret = zmq_ret + ", '_response_value': 'SUCCESS'";
1002 | }
1003 | }
1004 |
1005 | //+------------------------------------------------------------------+
1006 | // CLOSE ORDER (by Ticket)
1007 | void DWX_CloseOrder_Ticket(int _ticket, string &zmq_ret) {
1008 |
1009 | bool found = false;
1010 |
1011 | zmq_ret = zmq_ret + "'_action': 'CLOSE', '_ticket': " + IntegerToString(_ticket);
1012 |
1013 | for(int i=0; i= 0; i--) {
1046 | if (OrderSelect(i,SELECT_BY_POS)==true) {
1047 |
1048 | found = true;
1049 |
1050 | zmq_ret = zmq_ret + IntegerToString(OrderTicket()) + ": {'_symbol':'" + OrderSymbol() + "', '_magic': " + IntegerToString(OrderMagicNumber());
1051 |
1052 | if(OrderType() == OP_BUY || OrderType() == OP_SELL) {
1053 | DWX_CloseAtMarket(-1, zmq_ret);
1054 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_MARKET'";
1055 |
1056 | if (i != 0)
1057 | zmq_ret = zmq_ret + "}, ";
1058 | else
1059 | zmq_ret = zmq_ret + "}";
1060 |
1061 | } else {
1062 | zmq_ret = zmq_ret + ", '_response': 'CLOSE_PENDING'";
1063 |
1064 | if (i != 0)
1065 | zmq_ret = zmq_ret + "}, ";
1066 | else
1067 | zmq_ret = zmq_ret + "}";
1068 |
1069 | int tmpRet = OrderDelete(OrderTicket());
1070 | }
1071 | }
1072 | }
1073 |
1074 | zmq_ret = zmq_ret + "}";
1075 |
1076 | if(found == false) {
1077 | zmq_ret = zmq_ret + ", '_response': 'NOT_FOUND'";
1078 | }
1079 | else {
1080 | zmq_ret = zmq_ret + ", '_response_value': 'SUCCESS'";
1081 | }
1082 | }
1083 |
1084 | //+------------------------------------------------------------------+
1085 | // GET OPEN ORDERS
1086 | void DWX_GetOpenOrders(string &zmq_ret) {
1087 |
1088 | bool found = false;
1089 |
1090 | zmq_ret = zmq_ret + "'_action': 'OPEN_TRADES'";
1091 | zmq_ret = zmq_ret + ", '_trades': {";
1092 |
1093 | for(int i=OrdersTotal()-1; i>=0; i--) {
1094 | found = true;
1095 |
1096 | if (OrderSelect(i,SELECT_BY_POS)==true) {
1097 |
1098 | zmq_ret = zmq_ret + IntegerToString(OrderTicket()) + ": {";
1099 |
1100 | zmq_ret = zmq_ret + "'_magic': " + IntegerToString(OrderMagicNumber()) + ", '_symbol': '" + OrderSymbol() + "', '_lots': " + DoubleToString(OrderLots()) + ", '_type': " + IntegerToString(OrderType()) + ", '_open_price': " + DoubleToString(OrderOpenPrice()) + ", '_open_time': '" + TimeToStr(OrderOpenTime(),TIME_DATE|TIME_SECONDS) + "', '_SL': " + DoubleToString(OrderStopLoss()) + ", '_TP': " + DoubleToString(OrderTakeProfit()) + ", '_pnl': " + DoubleToString(OrderProfit()) + ", '_comment': '" + OrderComment() + "'";
1101 |
1102 | if (i != 0)
1103 | zmq_ret = zmq_ret + "}, ";
1104 | else
1105 | zmq_ret = zmq_ret + "}";
1106 | }
1107 | }
1108 | zmq_ret = zmq_ret + "}";
1109 |
1110 | }
1111 |
1112 | //+------------------------------------------------------------------+
1113 | // counts the number of orders with a given magic number. currently not used.
1114 | int DWX_numOpenOrdersWithMagic(int _magic) {
1115 | int n = 0;
1116 | for(int i=OrdersTotal()-1; i >= 0; i--) {
1117 | if (OrderSelect(i,SELECT_BY_POS)==true && OrderMagicNumber() == _magic) {
1118 | n++;
1119 | }
1120 | }
1121 | return n;
1122 | }
1123 |
1124 | //+------------------------------------------------------------------+
1125 | // CHECK IF TRADE IS ALLOWED
1126 | int DWX_IsTradeAllowed(int MaxWaiting_sec, string &zmq_ret) {
1127 |
1128 | if(!IsTradeAllowed()) {
1129 |
1130 | int StartWaitingTime = (int)GetTickCount();
1131 | zmq_ret = zmq_ret + ", " + "'_response': 'TRADE_CONTEXT_BUSY'";
1132 |
1133 | while(true) {
1134 |
1135 | if(IsStopped()) {
1136 | zmq_ret = zmq_ret + ", " + "'_response_value': 'EA_STOPPED_BY_USER'";
1137 | return(-1);
1138 | }
1139 |
1140 | int diff = (int)(GetTickCount() - StartWaitingTime);
1141 | if(diff > MaxWaiting_sec * 1000) {
1142 | zmq_ret = zmq_ret + ", '_response': 'WAIT_LIMIT_EXCEEDED', '_response_value': " + IntegerToString(MaxWaiting_sec);
1143 | return(-2);
1144 | }
1145 | // if the trade context has become free,
1146 | if(IsTradeAllowed()) {
1147 | zmq_ret = zmq_ret + ", '_response': 'TRADE_CONTEXT_NOW_FREE'";
1148 | RefreshRates();
1149 | return(1);
1150 | }
1151 |
1152 | }
1153 | } else {
1154 | return(1);
1155 | }
1156 | return(1);
1157 | }
1158 |
1159 | bool CheckServerStatus() {
1160 |
1161 | // Is _StopFlag == True, inform the client application
1162 | if (IsStopped()) {
1163 | InformPullClient(pullSocket, "{'_response': 'EA_IS_STOPPED'}");
1164 | return(false);
1165 | }
1166 |
1167 | // Default
1168 | return(true);
1169 | }
1170 |
1171 | string ErrorDescription(int error_code) {
1172 | string error_string;
1173 | //----
1174 | switch(error_code)
1175 | {
1176 | //---- codes returned from trade server
1177 | case 0:
1178 | case 1: error_string="no error"; break;
1179 | case 2: error_string="common error"; break;
1180 | case 3: error_string="invalid trade parameters"; break;
1181 | case 4: error_string="trade server is busy"; break;
1182 | case 5: error_string="old version of the client terminal"; break;
1183 | case 6: error_string="no connection with trade server"; break;
1184 | case 7: error_string="not enough rights"; break;
1185 | case 8: error_string="too frequent requests"; break;
1186 | case 9: error_string="malfunctional trade operation (never returned error)"; break;
1187 | case 64: error_string="account disabled"; break;
1188 | case 65: error_string="invalid account"; break;
1189 | case 128: error_string="trade timeout"; break;
1190 | case 129: error_string="invalid price"; break;
1191 | case 130: error_string="invalid stops"; break;
1192 | case 131: error_string="invalid trade volume"; break;
1193 | case 132: error_string="market is closed"; break;
1194 | case 133: error_string="trade is disabled"; break;
1195 | case 134: error_string="not enough money"; break;
1196 | case 135: error_string="price changed"; break;
1197 | case 136: error_string="off quotes"; break;
1198 | case 137: error_string="broker is busy (never returned error)"; break;
1199 | case 138: error_string="requote"; break;
1200 | case 139: error_string="order is locked"; break;
1201 | case 140: error_string="long positions only allowed"; break;
1202 | case 141: error_string="too many requests"; break;
1203 | case 145: error_string="modification denied because order too close to market"; break;
1204 | case 146: error_string="trade context is busy"; break;
1205 | case 147: error_string="expirations are denied by broker"; break;
1206 | case 148: error_string="amount of open and pending orders has reached the limit"; break;
1207 | case 149: error_string="hedging is prohibited"; break;
1208 | case 150: error_string="prohibited by FIFO rules"; break;
1209 | //---- mql4 errors
1210 | case 4000: error_string="no error (never generated code)"; break;
1211 | case 4001: error_string="wrong function pointer"; break;
1212 | case 4002: error_string="array index is out of range"; break;
1213 | case 4003: error_string="no memory for function call stack"; break;
1214 | case 4004: error_string="recursive stack overflow"; break;
1215 | case 4005: error_string="not enough stack for parameter"; break;
1216 | case 4006: error_string="no memory for parameter string"; break;
1217 | case 4007: error_string="no memory for temp string"; break;
1218 | case 4008: error_string="not initialized string"; break;
1219 | case 4009: error_string="not initialized string in array"; break;
1220 | case 4010: error_string="no memory for array\' string"; break;
1221 | case 4011: error_string="too long string"; break;
1222 | case 4012: error_string="remainder from zero divide"; break;
1223 | case 4013: error_string="zero divide"; break;
1224 | case 4014: error_string="unknown command"; break;
1225 | case 4015: error_string="wrong jump (never generated error)"; break;
1226 | case 4016: error_string="not initialized array"; break;
1227 | case 4017: error_string="dll calls are not allowed"; break;
1228 | case 4018: error_string="cannot load library"; break;
1229 | case 4019: error_string="cannot call function"; break;
1230 | case 4020: error_string="expert function calls are not allowed"; break;
1231 | case 4021: error_string="not enough memory for temp string returned from function"; break;
1232 | case 4022: error_string="system is busy (never generated error)"; break;
1233 | case 4050: error_string="invalid function parameters count"; break;
1234 | case 4051: error_string="invalid function parameter value"; break;
1235 | case 4052: error_string="string function internal error"; break;
1236 | case 4053: error_string="some array error"; break;
1237 | case 4054: error_string="incorrect series array using"; break;
1238 | case 4055: error_string="custom indicator error"; break;
1239 | case 4056: error_string="arrays are incompatible"; break;
1240 | case 4057: error_string="global variables processing error"; break;
1241 | case 4058: error_string="global variable not found"; break;
1242 | case 4059: error_string="function is not allowed in testing mode"; break;
1243 | case 4060: error_string="function is not confirmed"; break;
1244 | case 4061: error_string="send mail error"; break;
1245 | case 4062: error_string="string parameter expected"; break;
1246 | case 4063: error_string="integer parameter expected"; break;
1247 | case 4064: error_string="double parameter expected"; break;
1248 | case 4065: error_string="array as parameter expected"; break;
1249 | case 4066: error_string="requested history data in update state"; break;
1250 | case 4099: error_string="end of file"; break;
1251 | case 4100: error_string="some file error"; break;
1252 | case 4101: error_string="wrong file name"; break;
1253 | case 4102: error_string="too many opened files"; break;
1254 | case 4103: error_string="cannot open file"; break;
1255 | case 4104: error_string="incompatible access to a file"; break;
1256 | case 4105: error_string="no order selected"; break;
1257 | case 4106: error_string="unknown symbol"; break;
1258 | case 4107: error_string="invalid price parameter for trade function"; break;
1259 | case 4108: error_string="invalid ticket"; break;
1260 | case 4109: error_string="trade is not allowed in the expert properties"; break;
1261 | case 4110: error_string="longs are not allowed in the expert properties"; break;
1262 | case 4111: error_string="shorts are not allowed in the expert properties"; break;
1263 | case 4200: error_string="object is already exist"; break;
1264 | case 4201: error_string="unknown object property"; break;
1265 | case 4202: error_string="object is not exist"; break;
1266 | case 4203: error_string="unknown object type"; break;
1267 | case 4204: error_string="no object name"; break;
1268 | case 4205: error_string="object coordinates error"; break;
1269 | case 4206: error_string="no specified subwindow"; break;
1270 | default: error_string="unknown error";
1271 | }
1272 | return(error_string);
1273 | }
1274 |
1275 | //+------------------------------------------------------------------+
1276 |
1277 | double DWX_GetAsk(string symbol) {
1278 | if(symbol == "NULL") {
1279 | return(Ask);
1280 | } else {
1281 | return(MarketInfo(symbol,MODE_ASK));
1282 | }
1283 | }
1284 |
1285 | //+------------------------------------------------------------------+
1286 |
1287 | double DWX_GetBid(string symbol) {
1288 | if(symbol == "NULL") {
1289 | return(Bid);
1290 | } else {
1291 | return(MarketInfo(symbol,MODE_BID));
1292 | }
1293 | }
1294 | //+------------------------------------------------------------------+
1295 |
1296 | void printArray(string &arr[]) {
1297 | if (ArraySize(arr) == 0) Print("{}");
1298 | string printStr = "{";
1299 | int i;
1300 | for (i=0; i MetaTrader Connector
32 | """
33 |
34 | def __init__(self,
35 | _ClientID='dwx-zeromq', # Unique ID for this client
36 | _host='localhost', # Host to connect to
37 | _protocol='tcp', # Connection protocol
38 | _PUSH_PORT=32768, # Port for Sending commands
39 | _PULL_PORT=32769, # Port for Receiving responses
40 | _SUB_PORT=32770, # Port for Subscribing for prices
41 | _delimiter=';',
42 | _pulldata_handlers=[],
43 | # Handlers to process data received through PULL port.
44 | _subdata_handlers=[],
45 | # Handlers to process data received through SUB port.
46 | _verbose=True, # String delimiter
47 | _sleep_delay=0.001, # 1 ms for time.sleep()
48 | _monitor=False): # Experimental ZeroMQ Socket Monitoring
49 |
50 | self._logger = logging.getLogger('DWX_CONNECTOR')
51 | ######################################################################
52 |
53 | # Strategy Status (if this is False, ZeroMQ will not listen for data)
54 | self._ACTIVE = True
55 |
56 | # Client ID
57 | self._ClientID = _ClientID
58 |
59 | # ZeroMQ Host
60 | self._host = _host
61 |
62 | # Connection Protocol
63 | self._protocol = _protocol
64 |
65 | # ZeroMQ Context
66 | self._ZMQ_CONTEXT = zmq.asyncio.Context()
67 |
68 | # TCP Connection URL Template
69 | self._URL = self._protocol + "://" + self._host + ":"
70 |
71 | # Handlers for received data (pull and sub ports)
72 | self._pulldata_handlers = _pulldata_handlers
73 | self._subdata_handlers = _subdata_handlers
74 |
75 | # Ports for PUSH, PULL and SUB sockets respectively
76 | self._PUSH_PORT = _PUSH_PORT
77 | self._PULL_PORT = _PULL_PORT
78 | self._SUB_PORT = _SUB_PORT
79 |
80 | # Create Sockets
81 | self._PUSH_SOCKET = self._ZMQ_CONTEXT.socket(zmq.PUSH)
82 | self._PUSH_SOCKET.setsockopt(zmq.SNDHWM, 1)
83 | self._PUSH_SOCKET_STATUS = {'state': True, 'latest_event': 'N/A'}
84 |
85 | self._PULL_SOCKET = self._ZMQ_CONTEXT.socket(zmq.PULL)
86 | self._PULL_SOCKET.setsockopt(zmq.RCVHWM, 1)
87 | self._PULL_SOCKET_STATUS = {'state': True, 'latest_event': 'N/A'}
88 |
89 | self._SUB_SOCKET = self._ZMQ_CONTEXT.socket(zmq.SUB)
90 |
91 | # Bind PUSH Socket to send commands to MetaTrader
92 | self._PUSH_SOCKET.connect(self._URL + str(self._PUSH_PORT))
93 | self._logger.info("[INIT] Ready to send commands to METATRADER (PUSH): " +
94 | str(self._PUSH_PORT))
95 |
96 | # Connect PULL Socket to receive command responses from MetaTrader
97 | self._PULL_SOCKET.connect(self._URL + str(self._PULL_PORT))
98 | self._logger.info("[INIT] Listening for responses from METATRADER (PULL): " +
99 | str(self._PULL_PORT))
100 |
101 | # Connect SUB Socket to receive market data from MetaTrader
102 | self._logger.info("[INIT] Listening for market data from METATRADER (SUB): " +
103 | str(self._SUB_PORT))
104 | self._SUB_SOCKET.connect(self._URL + str(self._SUB_PORT))
105 |
106 | # Start listening for responses to commands and new market data
107 | self._string_delimiter = _delimiter
108 |
109 | # Market Data Dictionary by Symbol (holds tick data)
110 | self._Market_Data_DB = {} # {SYMBOL: {TIMESTAMP: (BID, ASK)}}
111 |
112 | # History Data Dictionary by Symbol (holds historic data of the last
113 | # HIST request for each symbol)
114 | # {SYMBOL_TF: [{'time': TIME, 'open': OPEN_PRICE, 'high': HIGH_PRICE,
115 | self._History_DB = {}
116 | # 'low': LOW_PRICE, 'close': CLOSE_PRICE, 'tick_volume': TICK_VOLUME,
117 | # 'spread': SPREAD, 'real_volume': REAL_VOLUME}, ...]}
118 |
119 | # Temporary Order STRUCT for convenience wrappers later.
120 | self.temp_order_dict = self._generate_default_order_dict()
121 |
122 | # Verbosity
123 | self._verbose = _verbose
124 |
125 | # Global Sleep Delay
126 | self._sleep_delay = _sleep_delay
127 |
128 | self._tasks = []
129 | self._tasks.append(
130 | asyncio.get_event_loop().create_task(
131 | self.zmq_pull_data(
132 | self._string_delimiter)))
133 | self._tasks.append(
134 | asyncio.get_event_loop().create_task(
135 | self.zmq_sub_data(
136 | self._string_delimiter)))
137 | # # Begin polling for PULL / SUB data
138 | # self._MarketData_Thread = Thread(target=self._DWX_ZMQ_Poll_Data_,
139 | # args=(self._string_delimiter,
140 | # self._poll_timeout,))
141 | # self._MarketData_Thread.daemon = True
142 | # self._MarketData_Thread.start()
143 |
144 | ###########################################
145 | # Enable/Disable ZeroMQ Socket Monitoring #
146 | ###########################################
147 | if _monitor == True:
148 |
149 | # ZeroMQ Monitor Event Map
150 | self._MONITOR_EVENT_MAP = {}
151 |
152 | self._logger.info(
153 | "\n[KERNEL] Retrieving ZeroMQ Monitor Event Names:\n")
154 |
155 | for name in dir(zmq):
156 | if name.startswith('EVENT_'):
157 | value = getattr(zmq, name)
158 | self._logger.info(f"{value}\t\t:\t{name}")
159 | self._MONITOR_EVENT_MAP[value] = name
160 |
161 | self._logger.info("\n[KERNEL] Socket Monitoring Config -> DONE!\n")
162 |
163 | # Disable PUSH/PULL sockets and let MONITOR events control them.
164 | self._PUSH_SOCKET_STATUS['state'] = False
165 | self._PULL_SOCKET_STATUS['state'] = False
166 |
167 | # PUSH
168 | self._tasks.append(asyncio.get_event_loop().create_task(
169 | self._DWX_ZMQ_EVENT_MONITOR_(
170 | 'PUSH', self._PUSH_SOCKET.get_monitor_socket())))
171 |
172 | # PULL
173 | self._tasks.append(asyncio.get_event_loop().create_task(
174 | self._DWX_ZMQ_EVENT_MONITOR_(
175 | 'PULL', self._PULL_SOCKET.get_monitor_socket())))
176 |
177 | ##########################################################################
178 |
179 | def _DWX_ZMQ_SHUTDOWN_(self):
180 |
181 | # Set INACTIVE
182 | self._ACTIVE = False
183 |
184 | for task in self._tasks:
185 | task.cancel()
186 |
187 | # Terminate context
188 | self._ZMQ_CONTEXT.destroy(0)
189 | self._logger.info(
190 | "\n++ [KERNEL] ZeroMQ Context Terminated.. shut down safely complete! :)")
191 |
192 | ##########################################################################
193 |
194 | """
195 | Set Status (to enable/disable strategy manually)
196 | """
197 |
198 | def _setStatus(self, _new_status=False):
199 |
200 | self._ACTIVE = _new_status
201 | self._logger.info(
202 | "\n**\n[KERNEL] Setting Status to {} - Deactivating Threads.. please wait a bit.\n**".format(_new_status))
203 |
204 | ##########################################################################
205 |
206 | """
207 | Function to send commands to MetaTrader (PUSH)
208 | """
209 |
210 | async def remote_send(self, _socket, _data):
211 |
212 | if self._PUSH_SOCKET_STATUS['state'] == True:
213 | try:
214 | await _socket.send_string(_data)
215 | except zmq.error.Again:
216 | self._logger.warn("\nResource timeout.. please try again.")
217 | await sleep(self._sleep_delay)
218 | else:
219 | self._logger.info(
220 | '\n[KERNEL] NO HANDSHAKE ON PUSH SOCKET.. Cannot SEND data')
221 |
222 | ##########################################################################
223 |
224 | """
225 | Function to retrieve data from MetaTrader (PULL)
226 | """
227 |
228 | async def remote_recv(self, _socket):
229 |
230 | if self._PULL_SOCKET_STATUS['state'] == True:
231 | try:
232 | msg = await _socket.recv_string()
233 | return msg
234 | except zmq.error.Again:
235 | self._logger.warn("\nResource timeout.. please try again.")
236 | await sleep(self._sleep_delay)
237 | else:
238 | self._logger.info(
239 | '\r[KERNEL] NO HANDSHAKE ON PULL SOCKET.. Cannot READ data',
240 | end='',
241 | flush=True)
242 |
243 | return None
244 |
245 | ##########################################################################
246 |
247 | # Convenience functions to permit easy trading via underlying functions.
248 |
249 | # OPEN ORDER
250 | async def _DWX_MTX_NEW_TRADE_(self, _order=None):
251 |
252 | if _order is None:
253 | _order = self._generate_default_order_dict()
254 |
255 | # Execute
256 | await self._DWX_MTX_SEND_COMMAND_(**_order)
257 |
258 | # MODIFY ORDER
259 | # _SL and _TP given in points. _price is only used for pending orders.
260 | async def _DWX_MTX_MODIFY_TRADE_BY_TICKET_(self, _ticket, _SL, _TP, _price=0):
261 |
262 | try:
263 | self.temp_order_dict['_action'] = 'MODIFY'
264 | self.temp_order_dict['_ticket'] = _ticket
265 | self.temp_order_dict['_SL'] = _SL
266 | self.temp_order_dict['_TP'] = _TP
267 | self.temp_order_dict['_price'] = _price
268 |
269 | # Execute
270 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
271 |
272 | except KeyError:
273 | self._logger.info(
274 | "[ERROR] Order Ticket {} not found!".format(_ticket))
275 |
276 | # CLOSE ORDER
277 | async def _DWX_MTX_CLOSE_TRADE_BY_TICKET_(self, _ticket):
278 |
279 | try:
280 | self.temp_order_dict['_action'] = 'CLOSE'
281 | self.temp_order_dict['_ticket'] = _ticket
282 |
283 | # Execute
284 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
285 |
286 | except KeyError:
287 | self._logger.info(
288 | "[ERROR] Order Ticket {} not found!".format(_ticket))
289 |
290 | # CLOSE PARTIAL
291 | async def _DWX_MTX_CLOSE_PARTIAL_BY_TICKET_(self, _ticket, _lots):
292 |
293 | try:
294 | self.temp_order_dict['_action'] = 'CLOSE_PARTIAL'
295 | self.temp_order_dict['_ticket'] = _ticket
296 | self.temp_order_dict['_lots'] = _lots
297 |
298 | # Execute
299 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
300 |
301 | except KeyError:
302 | self._logger.info(
303 | "[ERROR] Order Ticket {} not found!".format(_ticket))
304 |
305 | # CLOSE MAGIC
306 | async def _DWX_MTX_CLOSE_TRADES_BY_MAGIC_(self, _magic):
307 |
308 | try:
309 | self.temp_order_dict['_action'] = 'CLOSE_MAGIC'
310 | self.temp_order_dict['_magic'] = _magic
311 |
312 | # Execute
313 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
314 |
315 | except KeyError:
316 | pass
317 |
318 | # CLOSE ALL TRADES
319 | async def _DWX_MTX_CLOSE_ALL_TRADES_(self):
320 |
321 | try:
322 | self.temp_order_dict['_action'] = 'CLOSE_ALL'
323 |
324 | # Execute
325 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
326 |
327 | except KeyError:
328 | pass
329 |
330 | # GET OPEN TRADES
331 | async def _DWX_MTX_GET_ALL_OPEN_TRADES_(self):
332 |
333 | try:
334 | self.temp_order_dict['_action'] = 'GET_OPEN_TRADES'
335 |
336 | # Execute
337 | await self._DWX_MTX_SEND_COMMAND_(**self.temp_order_dict)
338 |
339 | except KeyError:
340 | pass
341 |
342 | # DEFAULT ORDER DICT
343 | def _generate_default_order_dict(self):
344 | return({'_action': 'OPEN',
345 | '_type': 0,
346 | '_symbol': 'EURUSD',
347 | '_price': 0.0,
348 | '_SL': 500, # SL/TP in POINTS, not pips.
349 | '_TP': 500,
350 | '_comment': self._ClientID,
351 | '_lots': 0.01,
352 | '_magic': 123456,
353 | '_ticket': 0})
354 |
355 | ##########################################################################
356 |
357 | """
358 | Function to construct messages for sending HIST commands to MetaTrader
359 |
360 | Because of broker GMT offset _end time might have to be modified.
361 | """
362 |
363 | async def _DWX_MTX_SEND_HIST_REQUEST_(self,
364 | _symbol='EURUSD',
365 | _timeframe=1440,
366 | _start='2020.01.01 00:00:00',
367 | _end=Timestamp.now().strftime('%Y.%m.%d %H:%M:00')):
368 | # _end='2019.01.04 17:05:00'):
369 |
370 | _msg = "{};{};{};{};{}".format('HIST',
371 | _symbol,
372 | _timeframe,
373 | _start,
374 | _end)
375 |
376 | # Send via PUSH Socket
377 | await self.remote_send(self._PUSH_SOCKET, _msg)
378 |
379 | ##########################################################################
380 | """
381 | Function to construct messages for sending TRACK_PRICES commands to
382 | MetaTrader for real-time price updates
383 | """
384 |
385 | async def _DWX_MTX_SEND_TRACKPRICES_REQUEST_(self,
386 | _symbols=['EURUSD']):
387 | _msg = 'TRACK_PRICES'
388 | for s in _symbols:
389 | _msg = _msg + ";{}".format(s)
390 |
391 | # Send via PUSH Socket
392 | await self.remote_send(self._PUSH_SOCKET, _msg)
393 |
394 | ##########################################################################
395 | """
396 | Function to construct messages for sending TRACK_RATES commands to
397 | MetaTrader for OHLC
398 | """
399 |
400 | async def _DWX_MTX_SEND_TRACKRATES_REQUEST_(self,
401 | _instruments=[('EURUSD_M1', 'EURUSD', 1)]):
402 | _msg = 'TRACK_RATES'
403 | for i in _instruments:
404 | _msg = _msg + ";{};{}".format(i[1], i[2])
405 |
406 | # Send via PUSH Socket
407 | await self.remote_send(self._PUSH_SOCKET, _msg)
408 |
409 | ##########################################################################
410 |
411 | ##########################################################################
412 | """
413 | Function to construct messages for sending Trade commands to MetaTrader
414 | """
415 |
416 | async def _DWX_MTX_SEND_COMMAND_(self, _action='OPEN', _type=0,
417 | _symbol='EURUSD', _price=0.0,
418 | _SL=50, _TP=50, _comment="Python-to-MT",
419 | _lots=0.01, _magic=123456, _ticket=0):
420 |
421 | _msg = "{};{};{};{};{};{};{};{};{};{};{}".format('TRADE', _action, _type,
422 | _symbol, _price,
423 | _SL, _TP, _comment,
424 | _lots, _magic,
425 | _ticket)
426 |
427 | # Send via PUSH Socket
428 | await self.remote_send(self._PUSH_SOCKET, _msg)
429 |
430 | """
431 | compArray[0] = TRADE or DATA
432 | compArray[1] = ACTION (e.g. OPEN, MODIFY, CLOSE)
433 | compArray[2] = TYPE (e.g. OP_BUY, OP_SELL, etc - only used when ACTION=OPEN)
434 |
435 | For compArray[0] == DATA, format is:
436 | DATA|SYMBOL|TIMEFRAME|START_DATETIME|END_DATETIME
437 |
438 | // ORDER TYPES:
439 | // https://docs.mql4.com/constants/tradingconstants/orderproperties
440 |
441 | // OP_BUY = 0
442 | // OP_SELL = 1
443 | // OP_BUYLIMIT = 2
444 | // OP_SELLLIMIT = 3
445 | // OP_BUYSTOP = 4
446 | // OP_SELLSTOP = 5
447 |
448 | compArray[3] = Symbol (e.g. EURUSD, etc.)
449 | compArray[4] = Open/Close Price (ignored if ACTION = MODIFY)
450 | compArray[5] = SL
451 | compArray[6] = TP
452 | compArray[7] = Trade Comment
453 | compArray[8] = Lots
454 | compArray[9] = Magic Number
455 | compArray[10] = Ticket Number (MODIFY/CLOSE)
456 | """
457 | # pass
458 |
459 | ##########################################################################
460 |
461 | """
462 | Function to check Poller for new reponses (PULL) and market data (SUB)
463 | """
464 |
465 | async def zmq_sub_data(self, string_delimiter=';'):
466 | while self._ACTIVE:
467 | await sleep(0)
468 | try:
469 | msg = await self._SUB_SOCKET.recv_string()
470 | if msg != "":
471 | _timestamp = str(Timestamp.now('UTC'))[:-6]
472 | _symbol, _data = msg.split(" ")
473 | if len(_data.split(string_delimiter)) == 2:
474 | _bid, _ask = _data.split(string_delimiter)
475 |
476 | if self._verbose:
477 | self._logger.info(
478 | "\n[" + _symbol + "] " + _timestamp + " (" + _bid + "/" + _ask + ") BID/ASK")
479 |
480 | # Update Market Data DB
481 | if _symbol not in self._Market_Data_DB.keys():
482 | self._Market_Data_DB[_symbol] = {}
483 |
484 | self._Market_Data_DB[_symbol][_timestamp] = (
485 | float(_bid), float(_ask))
486 |
487 | elif len(_data.split(string_delimiter)) == 8:
488 | _time, _open, _high, _low, _close, _tick_vol, _spread, _real_vol = _data.split(
489 | string_delimiter)
490 | if self._verbose:
491 | self._logger.debug(
492 | "[%s] %s (%s/%s/%s/%s/%s/%s/%s/%s) TIME/OPEN/HIGH/LOW/CLOSE/TICKVOL/SPREAD/VOLUME" %
493 | (_symbol, _timestamp, _time, _open, _high, _low, _close, _tick_vol, _spread, _real_vol))
494 | # Update Market Rate DB
495 | if _symbol not in self._Market_Data_DB.keys():
496 | self._Market_Data_DB[_symbol] = {}
497 | self._Market_Data_DB[_symbol][_timestamp] = (int(_time), float(_open), float(
498 | _high), float(_low), float(_close), int(_tick_vol), int(_spread), int(_real_vol))
499 |
500 | # invokes data handlers on sub port
501 | for hnd in self._subdata_handlers:
502 | await hnd.onSubData(msg)
503 |
504 | except zmq.error.Again:
505 | pass # resource temporarily unavailable, nothing to self._logger.info
506 | except ValueError:
507 | pass # No data returned, passing iteration.
508 | except UnboundLocalError:
509 | # _symbol may sometimes get referenced before being
510 | # assigned.
511 | pass
512 | except Exception as e:
513 | self._logger.exception(e)
514 |
515 | async def zmq_pull_data(self, string_delimiter=';'):
516 | while self._ACTIVE:
517 | await sleep(0)
518 | if self._PULL_SOCKET_STATUS['state'] == True:
519 | try:
520 | msg = await self.remote_recv(self._PULL_SOCKET)
521 | # If data is returned, store as pandas Series
522 | if msg != '' and msg is not None:
523 | try:
524 | _data = eval(msg)
525 | if '_action' in _data and _data['_action'] == 'HIST':
526 | _symbol = _data['_symbol']
527 | if '_data' in _data.keys():
528 | if _symbol not in self._History_DB.keys():
529 | self._History_DB[_symbol] = {}
530 | self._History_DB[_symbol] = _data['_data']
531 | else:
532 | self._logger.info(
533 | 'No data found. MT4 often needs multiple requests when accessing data of symbols without open charts.')
534 | self._logger.info('message: ' + msg)
535 |
536 | # invokes data handlers on pull port
537 | for hnd in self._pulldata_handlers:
538 | await hnd.onPullData(_data)
539 |
540 | if self._verbose:
541 | self._logger.info(_data) # default logic
542 |
543 | except Exception as ex:
544 | _exstr = "Exception Type {0}. Args:\n{1!r}"
545 | _msg = _exstr.format(
546 | type(ex).__name__, ex.args)
547 | self._logger.exception(_msg)
548 |
549 | except zmq.error.Again:
550 | pass # resource temporarily unavailable, nothing to self._logger.info
551 | except ValueError:
552 | pass # No data returned, passing iteration.
553 | except UnboundLocalError:
554 | # _symbol may sometimes get referenced before being
555 | # assigned.
556 | pass
557 | else:
558 | self._logger.info(
559 | '\r[KERNEL] NO HANDSHAKE on PULL SOCKET.. Cannot READ data.',
560 | end='',
561 | flush=True)
562 |
563 | ##########################################################################
564 |
565 | """
566 | Function to subscribe to given Symbol's BID/ASK feed from MetaTrader
567 | """
568 |
569 | async def _DWX_MTX_SUBSCRIBE_MARKETDATA_(self,
570 | _symbol='EURUSD',
571 | string_delimiter=';'):
572 |
573 | # Subscribe to SYMBOL first.
574 | self._SUB_SOCKET.setsockopt_string(zmq.SUBSCRIBE, _symbol)
575 |
576 | self._logger.info(
577 | "[KERNEL] Subscribed to {} BID/ASK updates. See self._Market_Data_DB.".format(_symbol))
578 |
579 | """
580 | Function to unsubscribe to given Symbol's BID/ASK feed from MetaTrader
581 | """
582 |
583 | async def _DWX_MTX_UNSUBSCRIBE_MARKETDATA_(self, _symbol):
584 |
585 | self._SUB_SOCKET.setsockopt_string(zmq.UNSUBSCRIBE, _symbol)
586 | self._logger.info(
587 | "\n**\n[KERNEL] Unsubscribing from " +
588 | _symbol +
589 | "\n**\n")
590 |
591 | """
592 | Function to unsubscribe from ALL MetaTrader Symbols
593 | """
594 |
595 | async def _DWX_MTX_UNSUBSCRIBE_ALL_MARKETDATA_REQUESTS_(self):
596 |
597 | # 31-07-2019 12:22 CEST
598 | for _symbol in self._Market_Data_DB.keys():
599 | await self._DWX_MTX_UNSUBSCRIBE_MARKETDATA_(_symbol=_symbol)
600 |
601 | ##########################################################################
602 |
603 | async def _DWX_ZMQ_EVENT_MONITOR_(self,
604 | socket_name,
605 | monitor_socket):
606 |
607 | # 05-08-2019 11:21 CEST
608 | while self._ACTIVE:
609 |
610 | # poll timeout is in ms, sleep() is s.
611 | await sleep(self._sleep_delay)
612 |
613 | # while monitor_socket.poll():
614 | while monitor_socket.poll(self._poll_timeout):
615 |
616 | try:
617 | evt = recv_monitor_message(monitor_socket)
618 | evt.update(
619 | {'description': self._MONITOR_EVENT_MAP[evt['event']]})
620 |
621 | # self._logger.info(f"\r[{socket_name} Socket] >> {evt['description']}", end='', flush=True)
622 | self._logger.info(
623 | f"\n[{socket_name} Socket] >> {evt['description']}")
624 |
625 | # Set socket status on HANDSHAKE
626 | if evt['event'] == 4096: # EVENT_HANDSHAKE_SUCCEEDED
627 |
628 | if socket_name == "PUSH":
629 | self._PUSH_SOCKET_STATUS['state'] = True
630 | self._PUSH_SOCKET_STATUS['latest_event'] = 'EVENT_HANDSHAKE_SUCCEEDED'
631 |
632 | elif socket_name == "PULL":
633 | self._PULL_SOCKET_STATUS['state'] = True
634 | self._PULL_SOCKET_STATUS['latest_event'] = 'EVENT_HANDSHAKE_SUCCEEDED'
635 |
636 | # self._logger.info(f"\n[{socket_name} Socket] >> ..ready for action!\n")
637 |
638 | else:
639 | # Update 'latest_event'
640 | if socket_name == "PUSH":
641 | self._PUSH_SOCKET_STATUS['state'] = False
642 | self._PUSH_SOCKET_STATUS['latest_event'] = evt['description']
643 |
644 | elif socket_name == "PULL":
645 | self._PULL_SOCKET_STATUS['state'] = False
646 | self._PULL_SOCKET_STATUS['latest_event'] = evt['description']
647 |
648 | if evt['event'] == zmq.EVENT_MONITOR_STOPPED:
649 |
650 | # Reinitialize the socket
651 | if socket_name == "PUSH":
652 | monitor_socket = self._PUSH_SOCKET.get_monitor_socket()
653 | elif socket_name == "PULL":
654 | monitor_socket = self._PULL_SOCKET.get_monitor_socket()
655 |
656 | except Exception as ex:
657 | _exstr = "Exception Type {0}. Args:\n{1!r}"
658 | _msg = _exstr.format(type(ex).__name__, ex.args)
659 | self._logger.info(_msg)
660 |
661 | # Close Monitor Socket
662 | monitor_socket.close()
663 |
664 | self._logger.info(
665 | f"\n++ [KERNEL] {socket_name} _DWX_ZMQ_EVENT_MONITOR_() Signing Out ++")
666 |
667 | ##########################################################################
668 |
669 | async def _DWX_ZMQ_HEARTBEAT_(self):
670 | await self.remote_send(self._PUSH_SOCKET, "HEARTBEAT;")
671 |
672 | ##########################################################################
673 |
674 | ##############################################################################
675 |
--------------------------------------------------------------------------------
/livetrader/market/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import *
2 | from .cache import *
3 | from .dwx import *
4 | from .tdx import *
5 |
--------------------------------------------------------------------------------
/livetrader/market/base.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from datetime import datetime
4 | from typing import List, Optional
5 |
6 | from livetrader.rpc import Method, Publisher
7 | from livetrader.utils import FifoQueue
8 |
9 |
10 | class MarketBase(object):
11 |
12 | __market_name__ = None
13 | __timeframe__ = None
14 |
15 | def connect(self):
16 | raise NotImplementedError()
17 |
18 | def disconnect(self):
19 | raise NotImplementedError()
20 |
21 | @property
22 | def logger(self):
23 | return logging.getLogger(self.__class__.__name__)
24 |
25 | async def watch_klines(self, symbol: str):
26 | raise NotImplementedError()
27 |
28 | async def get_kline_histories(self, symbol: str, from_ts: Optional[int] = None, limit: Optional[int] = None):
29 | raise NotImplementedError()
30 |
31 |
32 | class MarketService(object):
33 |
34 | def __init__(self, market: MarketBase, symbols: List[str]):
35 | self._market = market
36 | self._symbols = symbols
37 | self._tasks = []
38 | self.__logger__ = logging.getLogger('MarketService')
39 |
40 | async def _publish(self, symbol: str, queue: FifoQueue):
41 | async for kline in self._market.watch_klines(symbol):
42 | await queue.put((symbol, kline))
43 |
44 | def start(self):
45 | queue = FifoQueue(maxsize=100)
46 | self._market.connect()
47 | for symbol in self._symbols:
48 | self._tasks.append(asyncio.get_event_loop().create_task(
49 | self._publish(symbol, queue)))
50 | return queue
51 |
52 | @Method
53 | async def get_kline_histories(
54 | self, symbol: str, from_ts: Optional[datetime] = None, limit: Optional[int] = None):
55 | return list(await self._market.get_kline_histories(symbol, from_ts, limit))
56 |
57 | def stop(self):
58 | for task in self._tasks:
59 | task.cancel()
60 | self._market.disconnect()
61 |
--------------------------------------------------------------------------------
/livetrader/market/cache.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | from environs import Env
4 | from future.utils import iteritems
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 | from pymongo import ReplaceOne
7 | from livetrader.market import MarketBase
8 |
9 |
10 | class CachedMarket(MarketBase):
11 |
12 | def __init__(self, market: MarketBase, mongodb_uri: Optional[str] = None):
13 | self._env = env = Env()
14 | env.read_env()
15 | self._mongodb_uri = mongodb_uri if mongodb_uri else env.str(
16 | 'MONGODB_URI')
17 | self._market = market
18 | self._mongo_client = AsyncIOMotorClient(self._mongodb_uri)
19 | self._database = self._mongo_client.get_default_database('markets')
20 | self._hist_mongo_client = None
21 | self._initied = False
22 |
23 | def connect(self):
24 | self._market.connect()
25 |
26 | def disconnect(self):
27 | self._mongo_client.close()
28 | self._market.disconnect()
29 |
30 | def _collection(self, symbol: str):
31 | db = self._database
32 | if self._market.__class__.__market_name__ is None or self._market.__class__.__timeframe__ is None:
33 | raise Exception(
34 | 'Initialize market cache error (without market_name or timeframe)')
35 | collection_name = '%s_%s_%s' % (
36 | self._market.__class__.__market_name__, symbol, self._market.__class__.__timeframe__)
37 | collection = db[collection_name]
38 | return collection
39 |
40 | async def _insert_db(self, symbol: str, klines: List[dict]):
41 | collection = self._collection(symbol)
42 | await collection.bulk_write([
43 | ReplaceOne({'datetime': kline['datetime']}, kline, upsert=True)
44 | for kline in klines
45 | ])
46 |
47 | async def _initial_cache(self, symbol: str):
48 | collection = self._collection(symbol)
49 | if await collection.count_documents({}) > 0:
50 | rs = await (collection.find().sort([('datetime', -1)]).limit(1).to_list(1))
51 | last_kline = rs[0]
52 | history_klines = await self._market.get_kline_histories(
53 | symbol, from_ts=last_kline['datetime'])
54 | else:
55 | await collection.create_index('datetime', unique=True)
56 | history_klines = await self._market.get_kline_histories(
57 | symbol, limit=5000)
58 | await self._insert_db(symbol, history_klines)
59 |
60 | async def watch_klines(self, symbol: str):
61 | if not self._initied:
62 | self.logger.debug('init cache from watch_klines')
63 | await self._initial_cache(symbol)
64 | self._initied = True
65 | self.logger.debug('do watch klines from delegate')
66 | async for kline in self._market.watch_klines(symbol):
67 | await self._insert_db(symbol, [kline])
68 | yield kline
69 |
70 | async def get_kline_histories(self, symbol: str, from_ts: Optional[int] = None, limit: Optional[int] = None):
71 | if not self._initied:
72 | await self._initial_cache(symbol)
73 | self._initied = True
74 | collection = self._collection(symbol)
75 | criteria = {}
76 | if from_ts:
77 | criteria = {'datetime': {'$gte': from_ts}}
78 | cursor = collection.find(criteria).sort([('datetime', -1)])
79 | if limit:
80 | cursor = cursor.limit(limit)
81 | count = limit
82 | else:
83 | count = await collection.count_documents(criteria)
84 | return reversed(list(map(lambda x: {k: v for k, v in iteritems(x) if not k.startswith('_')}, await cursor.to_list(count))))
85 |
--------------------------------------------------------------------------------
/livetrader/market/dwx.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from asyncio import Queue, sleep
3 | from datetime import datetime
4 | from typing import Optional
5 |
6 | import pytz
7 | import tzlocal
8 | from environs import Env
9 | from pandas import Timedelta, Timestamp
10 | from pytz import timezone
11 | from livetrader.lib.dwx_zeromq_connector import DWX_ZeroMQ_Connector
12 | from livetrader.market import MarketBase
13 |
14 |
15 | class DwxMarket(MarketBase):
16 | __market_name__ = 'DWX'
17 | __timeframe__ = '1MIN'
18 |
19 | def __init__(self, host: Optional[str] = None,
20 | push_port: Optional[int] = None,
21 | pull_port: Optional[int] = None,
22 | sub_port: Optional[int] = None,
23 | time_zone: Optional[str] = None):
24 | self._env = env = Env()
25 | env.read_env()
26 | self._host = host if host else env.str('DWX_HOST')
27 | self._push_port = push_port if push_port else env.int(
28 | 'DWX_PUSH_PORT', 32768)
29 | self._pulling_port = pull_port if pull_port else env.int(
30 | 'DWX_PULL_PORT', 32769)
31 | self._subscribe_port = sub_port if sub_port else env.int(
32 | 'DWX_SUB_PORT', 32770)
33 | self._server_tz = timezone(
34 | time_zone if time_zone else env.str(
35 | 'DWX_TIMEZONE', 'EET'))
36 |
37 | def connect(self):
38 | self._kline_resp = {}
39 | self._kline_sub = {}
40 | self._connector = DWX_ZeroMQ_Connector(
41 | _host=self._host,
42 | _PUSH_PORT=self._push_port,
43 | _PULL_PORT=self._pulling_port,
44 | _SUB_PORT=self._subscribe_port,
45 | _subdata_handlers=[self],
46 | _pulldata_handlers=[self])
47 |
48 | def disconnect(self):
49 | self._connector._DWX_ZMQ_SHUTDOWN_()
50 |
51 | async def onSubData(self, msg):
52 | code, rates = msg.split(' ')
53 | code = code.split('_')[0]
54 | time, open, high, low, close, tick_volume, spread, real_volume = rates.split(
55 | ';')
56 | dt = self._server_tz.localize(
57 | datetime.utcfromtimestamp(int(time))).astimezone(pytz.utc)
58 | kline = {
59 | 'datetime': int(dt.timestamp()) * 1000,
60 | 'open': open,
61 | 'high': high,
62 | 'low': low,
63 | 'close': close,
64 | 'spread': spread,
65 | 'volume': int(tick_volume)
66 | }
67 | await self._kline_sub[code].put(kline)
68 |
69 | async def onPullData(self, msg):
70 | if msg['_action'] and msg['_action'] == 'HIST':
71 | queue = self._kline_resp[msg['_symbol'].split('_')[0]]
72 | await queue.put(msg['_data'])
73 |
74 | async def watch_klines(self, symbol: str):
75 | market, code = symbol.split('.')
76 | if market == 'FOREX':
77 | self._kline_sub[code] = Queue()
78 | await self._connector._DWX_MTX_SUBSCRIBE_MARKETDATA_(code)
79 | await self._connector._DWX_MTX_SEND_TRACKRATES_REQUEST_(
80 | [('%s_M1' % code, code, 1)])
81 | while True:
82 | kline = await self._kline_sub[code].get()
83 | yield kline
84 |
85 | async def get_kline_histories(self, symbol: str, from_ts: Optional[int] = None, limit: Optional[int] = None):
86 | market, code = symbol.split('.')
87 | if market == 'FOREX':
88 | local_tz = tzlocal.get_localzone()
89 | _end = local_tz.localize(Timestamp.now())
90 | if from_ts:
91 | _start = local_tz.localize(Timestamp.fromtimestamp(
92 | from_ts / 1000))
93 | else:
94 | _start = _end - Timedelta(minutes=limit - 1)
95 | if _start >= _end:
96 | return []
97 | _start = _start.astimezone(self._server_tz)
98 | _end = _end.astimezone(self._server_tz)
99 | self._kline_resp[code] = Queue()
100 | await self._connector._DWX_MTX_SEND_HIST_REQUEST_(_symbol=code,
101 | _timeframe=1,
102 | _start=_start.strftime(
103 | '%Y.%m.%d %H:%M:00'),
104 | _end=_end.strftime('%Y.%m.%d %H:%M:00'))
105 | klines = [self._parse_kline(kline_raw) for kline_raw in await self._kline_resp[code].get()]
106 | del self._kline_resp[code]
107 | return klines
108 | else:
109 | return []
110 |
111 | def _parse_kline(self, kline_raw: dict) -> dict:
112 | dt = self._server_tz.localize(
113 | datetime.strptime(
114 | kline_raw['time'],
115 | '%Y.%m.%d %H:%M')).astimezone(
116 | pytz.utc)
117 | return {
118 | 'datetime': int(dt.timestamp() * 1000),
119 | 'open': kline_raw['open'],
120 | 'high': kline_raw['high'],
121 | 'low': kline_raw['low'],
122 | 'close': kline_raw['close'],
123 | 'spread': kline_raw['spread'],
124 | 'volume': kline_raw['tick_volume']
125 | }
126 |
--------------------------------------------------------------------------------
/livetrader/market/tdx.py:
--------------------------------------------------------------------------------
1 | from asyncio import Event, sleep
2 | from datetime import datetime
3 | from typing import Optional
4 |
5 | import pytz
6 | from environs import Env
7 | from pytdx.exhq import TdxExHq_API, TDXParams
8 | from pytdx.parser.ex_get_instrument_bars import GetInstrumentBars
9 | from pytz import timezone
10 | from livetrader.exceptions import RemoteError
11 | from livetrader.market import MarketBase
12 |
13 |
14 | class TdxMarket(MarketBase):
15 | __market_name__ = 'TDX'
16 | __timeframe__ = '1MIN'
17 |
18 | def __init__(self, host: Optional[str] = None):
19 | self._env = env = Env()
20 | env.read_env()
21 | tdx_host = host if host else env.str('TDX_HOST')
22 | self._api = TdxExHq_API(heartbeat=True, multithread=True)
23 | self._ip, self._port = tdx_host.split(':')
24 | self._server_tz = timezone('Asia/Shanghai')
25 | self._pill = Event()
26 | self._market_mapping = dict(
27 | zip(["SHFE", "CZCE", "DCE", "CFFEX", "US"], [30, 28, 29, 47, 74]))
28 |
29 | def connect(self):
30 | if not self._api.connect(self._ip, int(self._port)):
31 | raise RemoteError('%s:%s connect error.' %
32 | (self.ip, int(self.port)))
33 | self._market_list = list(
34 | self._api.to_df(
35 | self._api.get_markets()).market)
36 |
37 | async def watch_klines(self, symbol: str):
38 | market, code = symbol.split('.')
39 | last_kline = None
40 | while not self._pill.is_set():
41 | tdx_klines = self._get_instrument_bars(
42 | TDXParams.KLINE_TYPE_1MIN, self._market_mapping[market], code, 0, 10)
43 | # 这个地方处理的原因在于由于是轮询的,就有可能出现在轮询间隔的时间(这里是1秒)内,有一半时间属于上一根K线,有一半时间属于下一根K线,所以如果我们只看最新的K线的话,前一根K线的一部分数据可能就会丢失,因此这里判断一下前一根K线如果有更新,就先把前一根K线推送了,再推送下一根K线
44 | prev_kline = self._parse_kline(tdx_klines[-2])
45 | latest_kline = self._parse_kline(tdx_klines[-1])
46 | if last_kline and prev_kline['datetime'] == last_kline['datetime']:
47 | self.logger.debug('yield prev kline: %s' % prev_kline)
48 | yield prev_kline
49 | if last_kline != latest_kline:
50 | yield latest_kline
51 | self.logger.debug('yield latest kline: %s' % latest_kline)
52 | last_kline = latest_kline
53 | await sleep(1)
54 |
55 | async def get_kline_histories(self, symbol: str, from_ts: Optional[int] = None, limit: Optional[int] = None):
56 | market, code = symbol.split('.')
57 | if self._market_mapping[market] in self._market_list:
58 | bars = []
59 | if from_ts is not None:
60 | idx = 0
61 | while len(bars) == 0 or bars[-1]['datetime'] >= from_ts:
62 | bars += [
63 | self._parse_kline(bar) for bar in reversed(
64 | self._get_instrument_bars(
65 | TDXParams.KLINE_TYPE_1MIN,
66 | self._market_mapping[market],
67 | code, idx * 700, 700))]
68 | idx += 1
69 | # 前面都是整700地取,所以有可能取到的数据会超过 from_ts,因此这里再做一次过滤
70 | bars = list(filter(lambda x: x['datetime'] >= from_ts, bars))
71 | elif limit is not None:
72 | for i in range(limit // 700):
73 | bars += [
74 | self._parse_kline(bar) for bar in reversed(
75 | self._get_instrument_bars(
76 | TDXParams.KLINE_TYPE_1MIN,
77 | self._market_mapping[market],
78 | code, i * 700, 700))]
79 | bars += [
80 | self._parse_kline(bar) for bar in reversed(
81 | self._get_instrument_bars(
82 | TDXParams.KLINE_TYPE_1MIN,
83 | self._market_mapping[market],
84 | code, limit // 700 * 700,
85 | limit % 700))]
86 | return reversed(bars)
87 |
88 | def disconnect(self):
89 | self._pill.set()
90 | self._api.disconnect()
91 |
92 | def _get_instrument_bars(self, category, market, code, start=0, count=700):
93 | while True:
94 | try:
95 | cmd = GetInstrumentBars(self._api.client, lock=self._api.lock)
96 | cmd.setParams(category, market, code, start=start, count=count)
97 | return cmd.call_api()
98 | except Exception as e:
99 | pass
100 |
101 | def _parse_kline(self, kline_tdx: dict) -> dict:
102 | dt = self._server_tz.localize(
103 | datetime.strptime(
104 | kline_tdx['datetime'],
105 | '%Y-%m-%d %H:%M')).astimezone(pytz.utc)
106 | return {
107 | 'datetime': int(dt.timestamp() * 1000),
108 | 'open': str(round(kline_tdx['open'], 2)),
109 | 'high': str(round(kline_tdx['high'], 2)),
110 | 'low': str(round(kline_tdx['low'], 2)),
111 | 'close': str(round(kline_tdx['close'], 2)),
112 | 'volume': kline_tdx['trade'],
113 | }
114 |
--------------------------------------------------------------------------------
/livetrader/rpc.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import functools
3 | import logging
4 | import signal
5 |
6 | import gevent
7 | import zerorpc
8 | from zerorpc import Client as _Client
9 | from zerorpc import Publisher as _Publisher
10 | from zerorpc import Subscriber as _Subscriber
11 |
12 | from livetrader.utils import FifoQueue
13 |
14 |
15 | class Publisher(_Publisher):
16 | pass
17 |
18 |
19 | class Subscriber(_Subscriber):
20 | pass
21 |
22 |
23 | class Client(_Client):
24 | pass
25 |
26 |
27 | class MarketSubscriber(Subscriber):
28 |
29 | def __init__(self, symbol: str):
30 | super().__init__()
31 | self._symbol = symbol
32 | self._pill2kill = asyncio.Event()
33 |
34 | def connect(self, endpoint, resolve=True):
35 | _endpoint = "%s_%s" % (endpoint, self._symbol)
36 | return super().connect(endpoint=_endpoint, resolve=resolve)
37 |
38 | def on_kline(self, kline: dict):
39 | raise NotImplementedError()
40 |
41 | def run(self):
42 | self._gevent_task = gevent.spawn(super().run)
43 |
44 | async def the_loop():
45 | while not self._pill2kill.is_set():
46 | await asyncio.sleep(0)
47 | gevent.sleep(0)
48 | self._asyncio_task = asyncio.get_event_loop().create_task(the_loop())
49 |
50 | def close(self):
51 | self._pill2kill.set()
52 | if self._gevent_task is not None:
53 | self._gevent_task.kill()
54 | if self._asyncio_task is not None:
55 | self._asyncio_task.cancel()
56 |
57 |
58 | class Method(object):
59 | def __init__(self, functor):
60 | self._functor = functor
61 | self.__doc__ = functor.__doc__
62 | self.__name__ = getattr(functor, "__name__", str(functor))
63 | functools.update_wrapper(self, functor)
64 |
65 | @property
66 | def coroutine(self):
67 | return asyncio.iscoroutinefunction(self._functor)
68 |
69 | def __get__(self, instance, type_instance=None):
70 | if instance is None:
71 | return self
72 | return self.__class__(self._functor.__get__(instance, type_instance))
73 |
74 | def __call__(self, *args, **kwargs):
75 | return self._functor(*args, **kwargs)
76 |
77 |
78 | class Server(object):
79 |
80 | def __init__(self, service):
81 | self._service = service
82 | self._publishers = {}
83 | self._logger = logging.getLogger('Server')
84 | methods = dict((k, self._decorate_coroutine_method(getattr(service, k))) for k in dir(service)
85 | if isinstance(getattr(service, k), Method))
86 | self.s = zerorpc.Server(methods=methods)
87 | self._pill2kill = asyncio.Event()
88 |
89 | def bind(self, endpoint: str):
90 | self._endpoint = endpoint
91 | self.s.bind(endpoint)
92 |
93 | def _decorate_coroutine_method(self, method):
94 | if method.coroutine:
95 | def __deco__(*args, **kwargs):
96 | return asyncio.get_event_loop().run_until_complete(method(*args, **kwargs))
97 | return __deco__
98 | else:
99 | return method
100 |
101 | def _publish(self, queue: FifoQueue):
102 | while not self._pill2kill.is_set():
103 | gevent.sleep(0)
104 | if not queue.empty():
105 | symbol, kline = queue.get_nowait()
106 | if symbol in self._publishers:
107 | publisher = self._publishers[symbol]
108 | else:
109 | publisher = self._publishers[symbol] = Publisher()
110 | publisher.bind('%s_%s' % (self._endpoint, symbol))
111 | publisher.on_kline(kline)
112 |
113 | def run(self):
114 | # register shutdown handler
115 | gevent.signal_handler(signal.SIGINT, self.close)
116 | gevent.signal_handler(signal.SIGTERM, self.close)
117 |
118 | queue = self._service.start()
119 | self._publish_task = gevent.spawn(self._publish, (queue))
120 | self._server_task = gevent.spawn(self.s.run)
121 |
122 | self._logger.info('Server started')
123 | while not self._pill2kill.is_set():
124 | asyncio.get_event_loop().run_until_complete(asyncio.sleep(0))
125 | gevent.sleep(0)
126 |
127 | def close(self):
128 | self._logger.info('Server stopping...')
129 | if self._publish_task is not None:
130 | self._publish_task.kill()
131 | self.s.close()
132 | if self._server_task is not None:
133 | self._server_task.kill()
134 | for publisher in self._publishers.values():
135 | publisher.close()
136 | self._service.stop()
137 | self._logger.info('Server stopped')
138 | self._pill2kill.set()
139 |
--------------------------------------------------------------------------------
/livetrader/trade/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lookis/livetrader/875c6f33906cd8d487daeef6e80c74fbe00be801/livetrader/trade/__init__.py
--------------------------------------------------------------------------------
/livetrader/trade/base.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import List, Optional
3 |
4 | from livetrader.rpc import Method, Publisher, Subscriber
5 |
6 |
7 | # 仅用来标识一些常量,具体order对象依旧是 dict 对象
8 | class OrderBase():
9 | ExecTypes = ['Market', 'Close', 'Limit', 'Stop', 'StopLimit', 'StopTrail',
10 | 'StopTrailLimit', 'Historical']
11 |
12 | OrdTypes = ['Buy', 'Sell']
13 | Buy, Sell = range(2)
14 |
15 | Created, Submitted, Accepted, Partial, Completed, \
16 | Canceled, Expired, Rejected = range(9)
17 |
18 | Cancelled = Canceled # alias
19 |
20 | Status = [
21 | 'Created', 'Submitted', 'Accepted', 'Partial', 'Completed',
22 | 'Canceled', 'Expired', 'Rejected',
23 | ]
24 |
25 |
26 | # 为了适配不同的交易接口,有些是推送数据(比方说期货)有些是拉数据(比方说数字货币),我们都用TradeBase 来代表,然后用 TradeService 封装出统一接口
27 | # 在 TradeBase 里如果是推送的接口,就直接 yield 出来,如果是拉数据的,就定时 polling,然后缓存在
28 | # TradeService 里(数据库),之后都统一出推送接口和polling 接口,这样TradeBase可以自己控制调用接口的频率,然后
29 | # TradeService 对外就没有调用限制了
30 |
31 |
32 | class TradeBase():
33 | def connect(self):
34 | raise NotImplementedError()
35 |
36 | def disconnect(self):
37 | raise NotImplementedError()
38 |
39 | async def fetch_balance(self):
40 | """
41 | balance 里包含字段:
42 | cash:现金
43 | value: 总资产
44 | """
45 | raise NotImplementedError()
46 |
47 | async def fetch_markets(self):
48 | """
49 | 获取可交易品种信息
50 | symbol 品种
51 | leverage 杠杆大小
52 | mult 乘数
53 | commission 费率
54 | margin 保证金
55 | shortable 可做空
56 | """
57 | raise NotImplementedError()
58 |
59 | async def create_order(self, symbol: str, ordtype: int, exectype: int, price: float, size: int, client: Optional[str] = None):
60 | """
61 | symbol: 标的,SHFE.rb2101
62 | ordtype: OrderBase.OrdTypes
63 | exectype: OrderBase.ExecTypes
64 | price: 价格
65 | size: 正数为 Long 多单, 负数为 Short 空单
66 | client: 客户端的id,调用用来区分是否为自己发起的订单(与成交)
67 |
68 | 返回:多一个 status 属性, OrderBase.Status
69 | 因为不是所有的市场关于定单都是阻塞的,就哪怕是市价单也会通过异步的方式通知订单状态,所以这里定单会有很多不同的状态,参考 OrderBase.Status
70 | """
71 | raise NotImplementedError()
72 |
73 | async def cancel_order(self, order_id: str):
74 | """
75 | 这里和 create_order 一样,也是异步调用,返回只能代表交易所收到这个请求,不一定执行成功,有可能会被拒绝,需要 watch_orders 看结果
76 | """
77 | raise NotImplementedError()
78 |
79 | async def watch_orders(self, symbol: str):
80 | raise NotImplementedError()
81 |
82 | async def fetch_orders(self, symbol: Optional[str] = None):
83 | raise NotImplementedError()
84 |
85 | async def fetch_open_orders(self, symbol: Optional[str] = None):
86 | raise NotImplementedError()
87 |
88 | async def fetch_trades(self, symbol: Optional[str] = None):
89 | raise NotImplementedError()
90 |
91 | async def watch_trades(self, symbol: str):
92 | raise NotImplementedError()
93 |
94 | async def fetch_positions(self, symbol: Optional[str] = None):
95 | raise NotImplementedError()
96 |
97 |
98 | class TradeSubscriber(Subscriber):
99 |
100 | def on_order(self, order: dict):
101 | raise NotImplementedError()
102 |
103 | def on_trade(self, trade: dict):
104 | raise NotImplementedError()
105 |
106 |
107 | class TradeService():
108 |
109 | def __init__(self):
110 | self._publishers = []
111 | self._tasks = []
112 |
113 | async def _publish(self):
114 | raise NotImplementedError()
115 |
116 | def start(self):
117 | # start publish service(polling and push)
118 | raise NotImplementedError()
119 |
120 | @Method
121 | async def get_balance(self):
122 | raise NotImplementedError()
123 |
124 | @Method
125 | async def create_order(self, symbol: str, exectype: str, price: float, size: int):
126 | raise NotImplementedError()
127 |
128 | @Method
129 | async def cancel_order(self, order_id: str):
130 | raise NotImplementedError()
131 |
132 | @Method
133 | async def open_orders(self, symbol: Optional[str] = None):
134 | raise NotImplementedError()
135 |
136 | @Method
137 | async def today_trades(self, symbol: Optional[str] = None):
138 | raise NotImplementedError()
139 |
140 | @Method
141 | async def fetch_position(self, symbol: Optional[str] = None):
142 | raise NotImplementedError()
143 |
144 | def stop(self):
145 | for publisher in self._publishers:
146 | publisher.close()
147 | for task in self._tasks:
148 | task.cancel()
149 |
--------------------------------------------------------------------------------
/livetrader/utils.py:
--------------------------------------------------------------------------------
1 |
2 | import asyncio
3 |
4 |
5 | class FifoQueue(asyncio.Queue):
6 |
7 | async def put(self, item):
8 | if self.maxsize > 0 and self.qsize() >= self.maxsize:
9 | await super().get()
10 | await super().put(item)
11 |
12 | def put_nowait(self, item):
13 | if self.maxsize > 0 and self.qsize() >= self.maxsize:
14 | super().get_nowait()
15 | return super().put_nowait(item)
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | environs
2 | zerorpc
3 | pymongo
4 | motor
5 | click
6 | tzlocal
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="livetrader", # Replace with your own username
8 | version="0.1",
9 | author='lookis',
10 | author_email="lookisliu@gmail.com",
11 | description="A small and extendable framework for trading",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/lookis/livetrader",
15 | packages=setuptools.find_packages(),
16 | install_requires=[
17 | 'click',
18 | 'tzlocal',
19 | 'environs',
20 | 'zerorpc',
21 | 'pymongo',
22 | 'motor',
23 | ],
24 | python_requires='>=3.6',
25 | )
26 |
--------------------------------------------------------------------------------